Anyolite / anyolite

Embedded mruby/Ruby for Crystal
https://anyolite.github.io/anyolite
MIT License
162 stars 10 forks source link

Raising and rescuing exceptions between Crystal and Ruby #22

Closed willhbr closed 2 years ago

willhbr commented 2 years ago

What is the best way of having exceptions propagate correctly between Ruby and Crystal?

As far as I can see the way to do this is the Anyolite.raise_*_error() macros, but as far as I can tell this doesn't allow Crystal to catch errors thrown in Ruby code, or for those errors to propagate back into the Ruby code that called the Crystal method.

I am currently doing some of this by checking the result of the return value from Anyolite.call_rb_block, checking if it is a Ruby Exception, and wrapping it in a custom Crystal exception with the same method and backtrace.

A few of the scenarios that I would like to handle are:

require "anyolite"

module Foo
  @[Anyolite::AddBlockArg(0, Nil)]
  def self.call_ruby_block_and_catch
    begin
      yield
    rescue err
      puts "I caught an error: #{err}"
    end
    puts "this should be printed"
    nil
  end

  def self.crystal_function_that_raises
    raise "oh no I failed"
    nil
  end

  @@block : Anyolite::RbRef? = nil

  @[Anyolite::StoreBlockArg]
  def self.store_block
    @@block = Anyolite.obtain_given_rb_block
    nil
  end

  def self.call_stored_block
    begin
      Anyolite.call_rb_block @@block.not_nil!
    rescue err
      puts "I caught the error"
      raise err
    end
    nil
  end
end

Anyolite::RbInterpreter.create do |rb|
  Anyolite.wrap(rb, Foo)

  rb.execute_script_line("Foo.call_ruby_block_and_catch { raise 'i am an error' }")
  rb.execute_script_line("Foo.store_block { raise 'another error' }
                          Foo.call_stored_block")
  rb.execute_script_line("begin
                            Foo.crystal_function_that_raises
                          rescue err
                            puts 'ruby caught error: ' + err.message
                          end
                         ")
end

Ideally, any exception raised by Crystal code would be wrapped into a custom Ruby exception, and vice versa. The simplest thing to do would be to catch any exception raised in Crystal and wrap it in a Anyolite.raise_runtime_error, and check the result of any Ruby block and raise that as a Crystal runtime error.

Help & pointers appreciated!

Hadeweka commented 2 years ago

This is a good point. I actually thought about this previously, but haven't found a good solution yet.

Catching exceptions from mruby calls should not be a problem, calling a Ruby script should already do that automatically. But the other way around is not so trivial.

Some of the general implementation ideas for Anyolite itself:

I will definitely try to find a solution for the next minor release, but for now it's still a design question.

Hadeweka commented 2 years ago

Update: I included simple safeguards to the Ruby function calling routines and tested whether they influence the performance in a significant way.

They didn't (the program even sped very slightly up for some reason), so the main branch now has a new commit, which introduces these safeguards.

They simply pass any catchable Crystal exception to Anyolite.raise_runtime_error as a simple message.

In the future, there could be a routine to differentiate between different exception types, but for now this should at least prevent some arithmetic overflow in a Crystal routine crashing the whole script.

I hope this helps - and please feel free to add any more suggestions to improve these routines :)

Hadeweka commented 2 years ago

Since there don't seem to be other problems, I will close this issue now. I also added some more improvement to exception handling (the Crystal backtrace is now visible and the exception types are a bit more helpful than simple runtime errors).

For further discussions, feel free to open a thread in the 'Discussions' tab (or here, if there is a bug).

willhbr commented 2 years ago

Sorry, haven't had any time to work on side projects recently and wanted to give this a proper shot with my project.

This looks like it'll do what I want, thanks! I'll let you know if there are any edge cases that I run into :)