Add mrbc_task_set_scheduler_hook for pre-scheduling deferred work - #306
Merged
Merged
Conversation
## 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
The hook is the deferred second half:
Timeline for the important case -- every task asleep:
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
What this adds
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.