Skip to content

Commit

Permalink
[GR-33908] [GR-34933] Support foreign exceptions fully and stop trans…
Browse files Browse the repository at this point in the history
…lating them to Ruby RuntimeError

PullRequest: truffleruby/3203
  • Loading branch information
eregon committed Mar 1, 2022
2 parents a628a3c + 7a14a37 commit e6753ad
Show file tree
Hide file tree
Showing 42 changed files with 725 additions and 205 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

New features:

* Foreign exceptions are now fully integrated and have most methods of `Exception` (@eregon).
* Foreign exceptions can now be rescued with `rescue Polyglot::ForeignException` or `rescue foreign_meta_object` (#2544, @eregon).

Bug fixes:

Expand Down
13 changes: 13 additions & 0 deletions doc/user/polyglot.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ If you want to pass a Ruby object to another language for fields to be read and

Where boolean value is expected (e.g., in `if` conditions) the foreign value is converted to boolean if possible or considered to be true.

### Rescuing Foreign Exceptions

Foreign exceptions can be caught by `rescue Polyglot::ForeignException => e` or by `rescue foreign_meta_object`.
It is possible to rescue any exception (Ruby or foreign) with `rescue Object => e`.

This naturally stems from the ancestors of a foreign exception:
```ruby
Java.type("java.lang.RuntimeException").new.class.ancestors
# => [Polyglot::ForeignExceptionClass, Polyglot::ForeignException, Polyglot::ForeignObject, Object, Kernel, BasicObject]
```

Note `Polyglot::ForeignException` is a module and not class, so to ensure it's included for all foreign exceptions.

## Accessing Java Objects

TruffleRuby's Java interoperability interface is similar to the interface from the Nashorn JavaScript implementation, as also implemented by GraalVM's JavaScript implementation.
Expand Down
8 changes: 7 additions & 1 deletion lib/truffle/truffle/ffi_backend/dynamic_library.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ def initialize(handle)

def self.open(libname, flags)
code = libname ? "load '#{libname}'" : 'default'
handle = Primitive.interop_eval_nfi code
begin
handle = Primitive.interop_eval_nfi code
rescue Polyglot::ForeignException => e
# Translate to a Ruby exception as it needs to be rescue'd by
# `rescue Exception` in FFI::Library#ffi_lib (which is part of the ffi gem)
raise RuntimeError, e.message
end
DynamicLibrary.new(libname, handle)
end

Expand Down
6 changes: 4 additions & 2 deletions spec/mspec/lib/mspec/matchers/raise_error.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class RaiseErrorMatcher
FAILURE_MESSAGE_FOR_EXCEPTION = {}.compare_by_identity

attr_writer :block

def initialize(exception, message, &block)
Expand All @@ -15,15 +17,15 @@ def initialize(exception, message, &block)
def matches?(proc)
@result = proc.call
return false
rescue Exception => actual
rescue Object => actual
@actual = actual

if matching_exception?(actual)
# The block has its own expectations and will throw an exception if it fails
@block[actual] if @block
return true
else
actual.instance_variable_set(:@mspec_raise_error_message, failure_message)
FAILURE_MESSAGE_FOR_EXCEPTION[actual] = failure_message
raise actual
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/mspec/lib/mspec/runner/exception.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def message

if @failure
message
elsif raise_error_message = @exception.instance_variable_get(:@mspec_raise_error_message)
elsif raise_error_message = RaiseErrorMatcher::FAILURE_MESSAGE_FOR_EXCEPTION[@exception]
raise_error_message.join("\n")
else
"#{@exception.class}: #{message}"
Expand Down
2 changes: 1 addition & 1 deletion spec/mspec/lib/mspec/runner/mspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def self.protect(location, &block)
raise e
rescue SkippedSpecError => e
return false
rescue Exception => exc
rescue Object => exc
register_exit 1
actions :exception, ExceptionState.new(current && current.state, location, exc)
return false
Expand Down
2 changes: 1 addition & 1 deletion spec/truffle/capi/unimplemented_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
it "raise a useful RuntimeError including the function name" do
-> {
@s.not_implemented_function("foo")
}.should raise_error(RuntimeError, /rb_str_shared_replace cannot be found/)
}.should raise_error(Polyglot::ForeignException, /rb_str_shared_replace cannot be found/)
end
end
13 changes: 12 additions & 1 deletion spec/truffle/interop/equal_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@

describe "Calling #equal? on a foreign object" do

it "tests reference equality" do
it "tests reference equality for an object which has an identity" do
a = Truffle::Debug.foreign_object
Truffle::Interop.should.has_identity?(a)
a.equal?(a).should be_true

b = Truffle::Debug.foreign_object
a.equal?(b).should be_false
end

it "tests reference equality for an object which has no identity" do
a = Truffle::Debug.foreign_object_with_members
Truffle::Interop.should_not.has_identity?(a)
a.equal?(a).should be_true

b = Truffle::Debug.foreign_object_with_members
a.equal?(b).should be_false
end

guard -> { !TruffleRuby.native? } do
it "looks at the underlying object for Java interop" do
big_integer = Truffle::Interop.java_type("java.math.BigInteger")
Expand Down
32 changes: 28 additions & 4 deletions spec/truffle/interop/foreign_inspect_to_s_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,35 @@
end
end

describe "Java exception" do
it "gives a similar representation to Ruby" do
integer_class = Truffle::Interop.java_type("java.lang.Integer")
-> {
integer_class.valueOf("abc")
}.should raise_error(Polyglot::ForeignException) { |exc|
exc.inspect.should =~ /\A#<Polyglot::ForeignExceptionClass\[Java\] java\.lang\.NumberFormatException:0x\h+: For input string: "abc">\z/
exc.to_s.should == '#<Polyglot::ForeignExceptionClass[Java] java.lang.NumberFormatException: For input string: "abc">'
}
end
end

describe "Java type" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Interop.java_type("java.math.BigInteger")
foreign.inspect.should == "#<Polyglot::ForeignInstantiable[Java] type java.math.BigInteger>"
foreign.to_s.should == "#<Polyglot::ForeignInstantiable[Java] type java.math.BigInteger>"
foreign.inspect.should == "#<Polyglot::ForeignClass[Java] type java.math.BigInteger>"
foreign.to_s.should == "#<Polyglot::ForeignClass[Java] type java.math.BigInteger>"

foreign = Truffle::Interop.java_type("int")
foreign.inspect.should == "#<Polyglot::ForeignMetaObject[Java] type int>"
foreign.to_s.should == "#<Polyglot::ForeignMetaObject[Java] type int>"
end
end

describe "Java java.lang.Class instance" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Interop.java_type("java.math.BigInteger")[:class]
foreign.inspect.should =~ /\A#<Polyglot::ForeignInstantiable\[Java\] java.lang.Class:0x\h+ java.math.BigInteger static={...}>\z/
foreign.to_s.should == "#<Polyglot::ForeignInstantiable[Java] java.math.BigInteger>"
foreign.inspect.should =~ /\A#<Polyglot::ForeignClass\[Java\] java\.lang\.Class:0x\h+ java\.math\.BigInteger static={\.\.\.}>\z/
foreign.to_s.should == "#<Polyglot::ForeignClass[Java] java.math.BigInteger>"
end
end

Expand Down Expand Up @@ -126,6 +142,14 @@
end
end

describe "exception" do
it "gives a similar representation to Ruby" do
exc = Truffle::Debug.foreign_exception("foo")
exc.inspect.should =~ /\A#<Polyglot::ForeignExceptionClass:0x\h+: foo>\z/
exc.to_s.should == '#<Polyglot::ForeignExceptionClass [foreign exception]>'
end
end

describe "object without members" do
it "gives a similar representation to Ruby" do
foreign = Truffle::Debug.foreign_object
Expand Down
40 changes: 40 additions & 0 deletions spec/truffle/interop/java_exception_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. This
# code is released under a tri EPL/GPL/LGPL license. You can use it,
# redistribute it and/or modify it under the terms of the:
#
# Eclipse Public License version 2.0, or
# GNU General Public License version 2, or
# GNU Lesser General Public License version 2.1.

require_relative '../../ruby/spec_helper'

guard -> { !TruffleRuby.native? } do
describe "Java exceptions" do
it "can be rescued with Polyglot::ForeignException" do
integer_class = Java.type("java.lang.Integer")

-> { integer_class.valueOf("abc") }.should raise_error(Polyglot::ForeignException)
end

it "can be rescued with java.lang.NumberFormatException" do
integer_class = Java.type("java.lang.Integer")
number_format_exception = Java.type("java.lang.NumberFormatException")

-> { integer_class.valueOf("abc") }.should raise_error(number_format_exception)
end

it "can be rescued with java.lang.RuntimeException" do
integer_class = Java.type("java.lang.Integer")
runtime_exception = Java.type("java.lang.RuntimeException")

-> { integer_class.valueOf("abc") }.should raise_error(runtime_exception)
end

it "can be rescued with java.lang.Throwable" do
integer_class = Java.type("java.lang.Integer")
throwable = Java.type("java.lang.Throwable")

-> { integer_class.valueOf("abc") }.should raise_error(throwable)
end
end
end
2 changes: 1 addition & 1 deletion spec/truffle/interop/java_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
end

it "throws RuntimeError for unknown class names" do
-> { Java.type("does.not.Exist") }.should raise_error(RuntimeError)
-> { Java.type("does.not.Exist") }.should raise_error(Polyglot::ForeignException)
end

it "works with symbols" do
Expand Down
2 changes: 1 addition & 1 deletion spec/truffle/interop/java_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
end

it "throws RuntimeError for unknown class names" do
-> { Truffle::Interop.java_type("does.not.Exist") }.should raise_error(RuntimeError)
-> { Truffle::Interop.java_type("does.not.Exist") }.should raise_error(Polyglot::ForeignException)
end

it "works with symbols" do
Expand Down
14 changes: 10 additions & 4 deletions spec/truffle/interop/polyglot/class_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

Truffle::Debug.foreign_hash.class.should == Polyglot::ForeignHash
Truffle::Debug.foreign_array.class.should == Polyglot::ForeignArray
Truffle::Debug.foreign_exception("").class.should == Polyglot::ForeignExceptionClass
Truffle::Debug.foreign_executable(14).class.should == Polyglot::ForeignExecutable
# ForeignInstantiable
# ForeignClass, ForeignMetaObject
Truffle::Debug.foreign_iterable.class.should == Polyglot::ForeignIterable
Truffle::Debug.foreign_iterator.class.should == Polyglot::ForeignIterator
Truffle::Debug.java_null.class.should == Polyglot::ForeignNull
Expand All @@ -30,8 +31,10 @@
it "gives a Ruby Class to Ruby objects behind a foreign proxy" do
Truffle::Interop.proxy_foreign_object({}).class.should == Polyglot::ForeignHashIterable
Truffle::Interop.proxy_foreign_object([1, 2, 3]).class.should == Polyglot::ForeignArray
Truffle::Interop.proxy_foreign_object(Exception.new("")).class.should == Polyglot::ForeignExceptionClass
Truffle::Interop.proxy_foreign_object(-> { nil }).class.should == Polyglot::ForeignExecutable
Truffle::Interop.proxy_foreign_object(String).class.should == Polyglot::ForeignInstantiable
Truffle::Interop.proxy_foreign_object(String).class.should == Polyglot::ForeignClass
Truffle::Interop.proxy_foreign_object(Enumerable).class.should == Polyglot::ForeignMetaObject
Truffle::Interop.proxy_foreign_object((1..3)).class.should == Polyglot::ForeignIterable
Truffle::Interop.proxy_foreign_object((1..3).each).class.should == Polyglot::ForeignIterableIterator
Truffle::Interop.proxy_foreign_object(nil).class.should == Polyglot::ForeignNull
Expand All @@ -48,8 +51,11 @@
Truffle::Interop.to_java_map({ a: 1 }).class.should == Polyglot::ForeignHash
Truffle::Interop.to_java_array([1, 2, 3]).class.should == Polyglot::ForeignArray
Truffle::Interop.to_java_list([1, 2, 3]).class.should == Polyglot::ForeignArray
Java.type('java.math.BigInteger').class.should == Polyglot::ForeignInstantiable
Java.type('java.math.BigInteger')[:class].class.should == Polyglot::ForeignInstantiable
Java.type('java.lang.RuntimeException').new.class.should == Polyglot::ForeignExceptionClass
# ForeignExecutable
Java.type('java.math.BigInteger').class.should == Polyglot::ForeignClass
Java.type('java.math.BigInteger')[:class].class.should == Polyglot::ForeignClass
Java.type('int').class.should == Polyglot::ForeignMetaObject
# ForeignIterable
Truffle::Interop.to_java_list([1, 2, 3]).iterator.class.should == Polyglot::ForeignIterator
Truffle::Debug.java_null.class.should == Polyglot::ForeignNull
Expand Down
70 changes: 70 additions & 0 deletions spec/truffle/interop/polyglot/foreign_exception_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. This
# code is released under a tri EPL/GPL/LGPL license. You can use it,
# redistribute it and/or modify it under the terms of the:
#
# Eclipse Public License version 2.0, or
# GNU General Public License version 2, or
# GNU Lesser General Public License version 2.1.

require_relative '../../../ruby/spec_helper'

describe "Polyglot::ForeignException" do
before :each do
@foreign = Truffle::Debug.foreign_exception("exception message")
end

it "supports #message" do
@foreign.message.should == "exception message"
end

it "supports #cause" do
@foreign.cause.should == nil
end

it "supports rescue Polyglot::ForeignException" do
begin
raise @foreign
rescue Polyglot::ForeignException => e
e.should.equal?(@foreign)
end
end

it "supports rescue Object" do
begin
raise @foreign
rescue Object => e
e.should.equal?(@foreign)
end
end

it "supports rescue class" do
begin
raise @foreign
rescue @foreign.class => e
e.should.equal?(@foreign)
end
end

it "supports #raise" do
-> { raise @foreign }.should raise_error(Polyglot::ForeignException) { |e|
e.should.equal?(@foreign)
}
end

it "supports #backtrace" do
@foreign.backtrace.should.is_a?(Array)
@foreign.backtrace.should_not.empty?
@foreign.backtrace.each { |entry| entry.should.is_a?(String) }
end

it "supports #backtrace_locations" do
@foreign.backtrace_locations.should.is_a?(Array)
@foreign.backtrace_locations.should_not.empty?
@foreign.backtrace_locations.each do |entry|
entry.should.respond_to?(:absolute_path)
entry.path.should.is_a?(String)
entry.lineno.should.is_a?(Integer)
entry.label.should.is_a?(String)
end
end
end
8 changes: 3 additions & 5 deletions spec/truffle/interop/source_location_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@
it "returns correct source location" do
method = Array.instance_method(:each_index)
Truffle::Interop.has_source_location?(method).should == true
Truffle::Interop.to_display_string(Truffle::Interop.source_location(method)).should include("array.rb")
Truffle::Interop.source_location(method).path.should.end_with?("/array.rb")

Truffle::Interop.has_source_location?(Array).should == true
Truffle::Interop.to_display_string(Truffle::Interop.source_location(Array)).should include("(unavailable)")

Truffle::Interop.has_source_location?(ObjectSpace).should == true
Truffle::Interop.to_display_string(Truffle::Interop.source_location(ObjectSpace)).should include("(unavailable)")
Truffle::Interop.source_location(Array).should_not.available?
Truffle::Interop.source_location(Array).path.should == "(unknown)"
end
end
2 changes: 2 additions & 0 deletions src/main/java/org/truffleruby/RubyLanguage.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import org.truffleruby.extra.ffi.RubyPointer;
import org.truffleruby.core.string.ImmutableRubyString;
import org.truffleruby.interop.RubyInnerContext;
import org.truffleruby.interop.RubySourceLocation;
import org.truffleruby.language.LexicalScope;
import org.truffleruby.language.RubyDynamicObject;
import org.truffleruby.language.RubyEvalInteractiveRootNode;
Expand Down Expand Up @@ -265,6 +266,7 @@ public static final class ThreadLocalState {
public final Shape prngRandomizerShape = createShape(RubyPRNGRandomizer.class);
public final Shape secureRandomizerShape = createShape(RubySecureRandomizer.class);
public final Shape sizedQueueShape = createShape(RubySizedQueue.class);
public final Shape sourceLocationShape = createShape(RubySourceLocation.class);
public final Shape stringShape = createShape(RubyString.class);
public final Shape syntaxErrorShape = createShape(RubySyntaxError.class);
public final Shape systemCallErrorShape = createShape(RubySystemCallError.class);
Expand Down
Loading

0 comments on commit e6753ad

Please sign in to comment.