Skip to content

Add mrbc_task_set_scheduler_hook for pre-scheduling deferred work - #306

Merged
HirohitoHigashi merged 1 commit into
mrubyc:masterfrom
hasumikin:task-scheduler-hook
Aug 6, 2026
Merged

HirohitoHigashi merged 1 commit into
mrubyc:masterfrom
hasumikin:task-scheduler-hook

Conversation

@hasumikin

Copy link
Copy Markdown
Member

Motivation

PicoRuby is building an ISR-to-task event bridge: interrupt handlers only stage work (set flags), and a deferred handler wakes tasks blocked in Task::Queue#pop. This needs a servicing point that runs in thread context BEFORE the scheduler reads the ready queue, so a task woken there is picked up in the same iteration. Today mruby/c has no such point: mrbc_hal_idle_cpu() runs only when the ready queue is empty, and nothing runs before task selection.

This mirrors mruby's mrb_task_set_scheduler_hook (mruby/mruby#6982); PicoRuby runs both VMs and keeps their scheduler surfaces symmetric.

How it works (UART RX as the example)

The ISR does two things only, neither of which touches the VM:

void uart_rx_isr(void)
{
  ring_buffer_put(uart_getc());  /* data into a plain C buffer */
  rx_flag = 1;                   /* volatile flag: "something arrived" */
}

The hook is the deferred second half:

void my_hook(void *ud)
{
  if (rx_flag) {
    rx_flag = 0;
    mrbc_task_queue_push(&q, &ev);  /* wakes the task blocked in q.pop */
  }
}

/* registered once at init */
mrbc_task_set_scheduler_hook(my_hook, NULL);

Timeline for the important case -- every task asleep:

task B:    ev = q.pop .......... queue empty, parks as WAITING
scheduler: no READY task -> mrbc_hal_idle_cpu()
                |
   UART interrupt fires!
   ISR: byte into buffer, rx_flag = 1, returns  (VM untouched)
                |
scheduler: returns from idle, reaches the loop top
   hook: sees rx_flag -> queue push -> task B becomes READY
   scheduler: reads the ready queue right after -> picks B
task B:    returns from q.pop, drains the ring buffer

An interrupt can fire at any moment -- possibly mid-allocation -- so an ISR must never push into a Task::Queue. The hook runs at a moment where the VM is by construction quiescent (between tasks, right before the ready-queue read), so it carries the ISR's note across to the safe side: the ISR leaves a mark, the hook performs the regular push.

Why pushing from the hook is safe

  • Thread context: mrbc_task_queue_push forbids interrupt context and VM re-entry (see the comment in c_task_queue.c); the hook's call sites are the bodies of mrbc_run/mrbc_run_step, which are neither.
  • No allocation is in flight at the loop top, so the push's array growth cannot corrupt an interrupted allocation.
  • The TCB relinking inside the push runs under the same mrbc_hal_disable_irq/enable_irq exclusion as any thread-context caller; the only concurrent mutator is the tick IRQ, which that exclusion covers.
  • No flag_preemption is needed: the very next statement reads the ready queue, so a task woken here is selected immediately.

What this adds

  • mrbc_task_set_scheduler_hook(fn, ud): a process-global hook invoked in thread context right before the ready-queue read, at both scheduler entries (the mrbc_run loop top and mrbc_run_step). mruby's hook is per-mrb_state; mruby/c's is process-global because the scheduler queues are process-global (VMs live per TCB).
  • The whole feature sits behind #if defined(MRBC_TASK_SCHEDULER_HOOK): default builds compile it out entirely -- zero cost and zero behavior change for existing applications and HALs. Enabled builds pay one pointer check per scheduler iteration while no hook is set.
  • Single-owner contract (documented in rrt0.h): setting replaces, fn == NULL clears, composition is the embedder's concern, the caller owns ud. The hook must be cheap, never sleep, and never re-enter the scheduler.
  • No HAL involvement: gems/embedders register at init, so no port needs changes.

Behavioral tests for the contract live in PicoRuby's integration suite; the mruby side carries equivalent tests in mruby-task's own mrbtest suite, while mruby/c's pure-Ruby test harness cannot register a C hook.

## Motivation

PicoRuby is building an ISR-to-task event bridge: interrupt handlers
only stage work (set flags), and a deferred handler wakes tasks
blocked in Task::Queue#pop. This needs a servicing point that runs in
thread context BEFORE the scheduler reads the ready queue, so a task
woken there is picked up in the same iteration. Today mruby/c has no
such point: mrbc_hal_idle_cpu() runs only when the ready queue is
empty, and nothing runs before task selection.

This mirrors mruby's mrb_task_set_scheduler_hook (mruby/mruby#6982);
PicoRuby runs both VMs and keeps their scheduler surfaces symmetric.

## How it works (UART RX as the example)

The ISR does two things only, neither of which touches the VM:

    void uart_rx_isr(void)
    {
      ring_buffer_put(uart_getc());  /* data into a plain C buffer */
      rx_flag = 1;                   /* volatile flag: "something arrived" */
    }

The hook is the deferred second half:

    void my_hook(void *ud)
    {
      if (rx_flag) {
        rx_flag = 0;
        mrbc_task_queue_push(&q, &ev);  /* wakes the task blocked in q.pop */
      }
    }

    /* registered once at init */
    mrbc_task_set_scheduler_hook(my_hook, NULL);

Timeline for the important case -- every task asleep:

    task B:    ev = q.pop .......... queue empty, parks as WAITING
    scheduler: no READY task -> mrbc_hal_idle_cpu()
                    |
       UART interrupt fires!
       ISR: byte into buffer, rx_flag = 1, returns  (VM untouched)
                    |
    scheduler: returns from idle, reaches the loop top
       hook: sees rx_flag -> queue push -> task B becomes READY
       scheduler: reads the ready queue right after -> picks B
    task B:    returns from q.pop, drains the ring buffer

An interrupt can fire at any moment -- possibly mid-allocation -- so
an ISR must never push into a Task::Queue. The hook runs at a moment
where the VM is by construction quiescent (between tasks, right
before the ready-queue read), so it carries the ISR's note across to
the safe side: the ISR leaves a mark, the hook performs the regular
push.

## Why pushing from the hook is safe

- Thread context: mrbc_task_queue_push forbids interrupt context and
  VM re-entry (see the comment in c_task_queue.c); the hook's call
  sites are the bodies of mrbc_run/mrbc_run_step, which are neither.
- No allocation is in flight at the loop top, so the push's array
  growth cannot corrupt an interrupted allocation.
- The TCB relinking inside the push runs under the same
  mrbc_hal_disable_irq/enable_irq exclusion as any thread-context
  caller; the only concurrent mutator is the tick IRQ, which that
  exclusion covers.
- No flag_preemption is needed: the very next statement reads the
  ready queue, so a task woken here is selected immediately.

## What this adds

- mrbc_task_set_scheduler_hook(fn, ud): a process-global hook invoked
  in thread context right before the ready-queue read, at both
  scheduler entries (the mrbc_run loop top and mrbc_run_step).
  mruby's hook is per-mrb_state; mruby/c's is process-global because
  the scheduler queues are process-global (VMs live per TCB).
- The whole feature sits behind #if defined(MRBC_TASK_SCHEDULER_HOOK):
  default builds compile it out entirely -- zero cost and zero
  behavior change for existing applications and HALs. Enabled builds
  pay one pointer check per scheduler iteration while no hook is set.
- Single-owner contract (documented in rrt0.h): setting replaces,
  fn == NULL clears, composition is the embedder's concern, the
  caller owns ud. The hook must be cheap, never sleep, and never
  re-enter the scheduler.
- No HAL involvement: gems/embedders register at init, so no port
  needs changes.

Behavioral tests for the contract live in PicoRuby's integration
suite; the mruby side carries equivalent tests in mruby-task's own
mrbtest suite, while mruby/c's pure-Ruby test harness cannot register
a C hook.
@HirohitoHigashi
HirohitoHigashi merged commit b5c5a86 into mrubyc:master Aug 6, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants