From 2fef9422cf57161295abc4e8a2cbfc62d5571676 Mon Sep 17 00:00:00 2001 From: devs6186 Date: Sun, 15 Mar 2026 19:55:15 +0530 Subject: [PATCH] fix(rb_loader): return TYPE_THROWABLE from invoke path on Ruby exception MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a Ruby function raises an exception, rb_protect() returns a non-zero state but the invoke path fell through to rb_type_deserialize() on Qnil, silently returning a TYPE_NULL value to the caller. The four TODO comments marking this were unfixed since the issue was opened. Replace the rb_loader_impl_print_last_exception macro (which only logged) with a static helper rb_loader_impl_exception_value() that extracts the pending exception from rb_errinfo(), builds a TYPE_THROWABLE value using exception_create_const/throwable_create/value_create_throwable — the same pattern used by py_loader_impl_error_value_from_exception() — and returns it immediately. The error is cleared with rb_set_errinfo(Qnil) so it does not propagate further up the Ruby stack. All four invoke branches (TYPED, DUCKTYPED, MIXED, zero-args) now return a structured throwable carrying the exception class name, message, and first backtrace line. Node.js callers receive a native JS Error via the existing TYPE_THROWABLE handling in node_loader_impl_value_to_napi(), and C callers can extract the exception with metacall_error_from_value(). Fixes #141 --- .../loaders/rb_loader/source/rb_loader_impl.c | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/source/loaders/rb_loader/source/rb_loader_impl.c b/source/loaders/rb_loader/source/rb_loader_impl.c index 67b9a7805e..afa1d830d8 100644 --- a/source/loaders/rb_loader/source/rb_loader_impl.c +++ b/source/loaders/rb_loader/source/rb_loader_impl.c @@ -345,22 +345,32 @@ static VALUE rb_loader_impl_funcallv_kw_protect(VALUE args) return rb_funcallv_kw(protect->module_instance, protect->id, protect->argc, protect->argv, RB_PASS_KEYWORDS); } -/* TODO: Convert this into a return exception */ -#define rb_loader_impl_print_last_exception() \ - do \ - { \ - VALUE e = rb_errinfo(); \ - if (e != Qnil) \ - { \ - VALUE error; \ - VALUE bt = rb_funcall(e, rb_intern("backtrace"), 0); \ - VALUE msg = rb_funcall(e, rb_intern("message"), 0); \ - bt = rb_ary_entry(bt, 0); \ - error = rb_sprintf("%" PRIsVALUE ": %" PRIsVALUE " (%s)\n", bt, msg, rb_obj_classname(e)); \ - log_write("metacall", LOG_LEVEL_ERROR, "Exception raised in Ruby '%s'", RSTRING_PTR(error)); \ - rb_backtrace(); \ - } \ - } while (0) +static value rb_loader_impl_exception_value(void) +{ + static const char backtrace_not_found[] = "Backtrace not available"; + VALUE e = rb_errinfo(); + + if (e == Qnil) + { + return NULL; + } + + VALUE msg_obj = rb_funcall(e, rb_intern("message"), 0); + VALUE bt_ary = rb_funcall(e, rb_intern("backtrace"), 0); + VALUE bt_first = (bt_ary != Qnil && RARRAY_LEN(bt_ary) > 0) ? rb_ary_entry(bt_ary, 0) : Qnil; + + const char *class_name = rb_obj_classname(e); + const char *msg = (msg_obj != Qnil) ? StringValuePtr(msg_obj) : ""; + const char *bt = (bt_first != Qnil) ? StringValuePtr(bt_first) : backtrace_not_found; + + log_write("metacall", LOG_LEVEL_ERROR, "Exception raised in Ruby '%s': %s", class_name, msg); + + rb_set_errinfo(Qnil); + + exception ex = exception_create_const(msg, class_name, 0, bt); + throwable th = throwable_create(value_create_exception(ex)); + return value_create_throwable(th); +} function_return function_rb_interface_invoke(function func, function_impl impl, function_args args, size_t size) { @@ -446,9 +456,7 @@ function_return function_rb_interface_invoke(function func, function_impl impl, if (state != 0) { - rb_loader_impl_print_last_exception(); - - // TODO: Throw exception? + return rb_loader_impl_exception_value(); } } else if (invoke_type == FUNCTION_RB_DUCKTYPED) @@ -465,9 +473,7 @@ function_return function_rb_interface_invoke(function func, function_impl impl, if (state != 0) { - rb_loader_impl_print_last_exception(); - - // TODO: Throw exception ? + return rb_loader_impl_exception_value(); } } else if (invoke_type == FUNCTION_RB_MIXED) @@ -486,9 +492,7 @@ function_return function_rb_interface_invoke(function func, function_impl impl, if (state != 0) { - rb_loader_impl_print_last_exception(); - - // TODO: Throw exception ? + return rb_loader_impl_exception_value(); } } } @@ -506,9 +510,7 @@ function_return function_rb_interface_invoke(function func, function_impl impl, if (state != 0) { - rb_loader_impl_print_last_exception(); - - // TODO: Throw exception ? + return rb_loader_impl_exception_value(); } }