diff --git a/include/mruby.h b/include/mruby.h index 7bbfb9634b..4a51b09730 100644 --- a/include/mruby.h +++ b/include/mruby.h @@ -275,6 +275,7 @@ typedef struct mrb_task_state { struct mrb_task *main_task; /* Main task wrapper for root context */ uint8_t scheduler_lock; /* Lock counter for synchronous execution */ mrb_bool loop_running; /* Active mrb_task_run loop flag */ + mrb_bool exception_as_result; /* Return unhandled task exceptions as values */ } mrb_task_state; #endif diff --git a/mrbgems/mruby-task/src/task.c b/mrbgems/mruby-task/src/task.c index 5aa47c1ef4..2c3df174f9 100644 --- a/mrbgems/mruby-task/src/task.c +++ b/mrbgems/mruby-task/src/task.c @@ -314,6 +314,27 @@ task_change_state(mrb_state *mrb, mrb_task *t, uint8_t new_status) mrb_task_enable_irq(); } +typedef struct execute_task_vm_args { + mrb_task *t; + const struct RProc *proc; + const mrb_code *pc; +} execute_task_vm_args; + +static mrb_value +execute_task_vm(mrb_state *mrb, void *ud) +{ + execute_task_vm_args *args = (execute_task_vm_args*)ud; + + mrb->task.exception_as_result = TRUE; + args->t->result = mrb_vm_exec(mrb, args->proc, args->pc); + if (mrb->exc) { + args->t->result = mrb_obj_value(mrb->exc); + mrb->exc = NULL; + } + mrb->task.exception_as_result = FALSE; + return args->t->result; +} + /* Execute a single task - core task execution logic */ static void execute_task(mrb_state *mrb, mrb_task *t) @@ -348,8 +369,13 @@ execute_task(mrb_state *mrb, mrb_task *t) /* Set vmexec flag to prevent fiber_terminate from being called */ t->c.vmexec = TRUE; - /* Execute task - PC is saved in ci->pc from previous run */ - t->result = mrb_vm_exec(mrb, proc, pc); + /* Execute task - PC is saved in ci->pc from previous run. + Unhandled task exceptions are converted to the task result by + mrb_vm_exec() in task mode, so the scheduler protect frame stays intact. */ + execute_task_vm_args args = { t, proc, pc }; + mrb_bool error = FALSE; + t->result = mrb_protect_error(mrb, execute_task_vm, &args, &error); + mrb->task.exception_as_result = FALSE; /* Clear vmexec flag */ t->c.vmexec = FALSE; @@ -363,6 +389,18 @@ execute_task(mrb_state *mrb, mrb_task *t) prev_c->ci = prev_ci; prev_ci->cci = prev_cci; + /* If an abnormal path inside mrb_vm_exec() bypassed + exception_as_result and unwound via MRB_THROW (e.g. a + CINFO_SKIP frame), mrb_protect_error caught it and stored the + exception object in t->result. Force the task to terminate + cleanly so the scheduler keeps running instead of aborting - + re-raising into the scheduler would abort in pattern 1, where + no outer jmpbuf exists. The exception remains observable via + mrb_task_value() / Task#value. */ + if (error) { + t->c.status = MRB_TASK_STOPPED; + } + /* Handle task termination */ if (t->c.status == MRB_TASK_STOPPED) { switching_ = FALSE; @@ -1524,6 +1562,7 @@ mrb_mruby_task_gem_init(mrb_state *mrb) mrb->task.main_task = NULL; mrb->task.scheduler_lock = 0; mrb->task.loop_running = FALSE; + mrb->task.exception_as_result = FALSE; task_class = mrb_define_class_id(mrb, MRB_SYM(Task), mrb->object_class); MRB_SET_INSTANCE_TT(task_class, MRB_TT_DATA); @@ -1555,6 +1594,7 @@ mrb_mruby_task_gem_init(mrb_state *mrb) mrb_define_method_id(mrb, task_class, MRB_SYM(resume), mrb_task_resume, MRB_ARGS_NONE()); mrb_define_method_id(mrb, task_class, MRB_SYM(terminate), mrb_task_terminate, MRB_ARGS_NONE()); mrb_define_method_id(mrb, task_class, MRB_SYM(join), mrb_task_join, MRB_ARGS_NONE()); + mrb_define_method_id(mrb, task_class, MRB_SYM(value), mrb_task_value, MRB_ARGS_NONE()); /* Kernel methods (module functions like CRuby) * Note: sleep and usleep override mruby-sleep's implementation to be task-aware diff --git a/mrbgems/mruby-task/test/task.rb b/mrbgems/mruby-task/test/task.rb index 678b136aba..5bbe844e30 100644 --- a/mrbgems/mruby-task/test/task.rb +++ b/mrbgems/mruby-task/test/task.rb @@ -194,3 +194,17 @@ Task.run end end + +assert("Task#value returns exception object for unhandled task errors") do + child = nil + + Task.new do + child = Task.new { raise "boom" } + end + + Task.run + + result = child.value + assert_kind_of RuntimeError, result + assert_equal "boom", result.message +end diff --git a/src/vm.c b/src/vm.c index bb0c877f18..0a84798b0d 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1635,9 +1635,11 @@ task_across_c_boundary(mrb_state *mrb) if (mrb->c->status != MRB_TASK_STOPPED) \ mrb->c->status = MRB_TASK_STOPPED; \ } while (0) +#define TASK_RETURN_EXCEPTION_AS_VALUE(mrb) ((mrb)->task.exception_as_result) #else #define RETURN_IF_TASK_STOPPED(mrb) #define TASK_STOP(mrb) +#define TASK_RETURN_EXCEPTION_AS_VALUE(mrb) FALSE #endif /** @@ -2691,6 +2693,7 @@ mrb_vm_exec(mrb_state *mrb, const struct RProc *begin_proc, const mrb_code *iseq fiber_terminate(mrb, c, ci); if (mrb_unlikely(!c->vmexec)) goto L_RAISE; mrb->jmp = prev_jmp; + if (TASK_RETURN_EXCEPTION_AS_VALUE(mrb)) return mrb_obj_value(mrb->exc); if (!prev_jmp) return mrb_obj_value(mrb->exc); MRB_THROW(prev_jmp); }