Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/mruby.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
44 changes: 42 additions & 2 deletions mrbgems/mruby-task/src/task.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions mrbgems/mruby-task/test/task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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);
}
Expand Down
Loading