Files
Yukihiro "Matz" Matsumoto 143959b94b doc: fix prettier formatting in markdown files
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-12 14:33:37 +09:00

326 lines
9.5 KiB
Markdown

<!-- summary: Virtual Machine Internals -->
# Virtual Machine Internals
This document describes mruby's virtual machine for developers
working on `src/vm.c` and related code.
**Read this if you are:** debugging method dispatch or call frame
issues, working on exception handling, implementing new opcodes,
modifying fiber/coroutine behavior, or optimizing the dispatch loop.
For the instruction set, see [opcode.md](opcode.md). For the
compiler that generates bytecode, see [compiler.md](compiler.md).
## Execution Model
mruby uses a **register-based VM**. Local variables and temporaries
occupy fixed register slots determined at compile time. Each method
call gets its own register window on a shared value stack.
## Execution Context
The VM state is stored in `mrb_context`:
```text
mrb_context
+-- stbase..stend value stack (mrb_value[])
+-- cibase..ciend call info stack (mrb_callinfo[])
+-- ci current call frame pointer
+-- status fiber state
+-- prev previous context (fiber chain)
+-- vmexec VM execution state flag
```
The value stack and call info stack grow independently. Each fiber
has its own `mrb_context`.
### Stack Sizing
- Initial value stack: 128 entries (`STACK_INIT_SIZE`)
- Initial call info stack: 32 entries (`CALLINFO_INIT_SIZE`)
- Growth factor: 1.5x (or 2x with `MRB_STACK_EXTEND_DOUBLING`)
- Minimum growth: 128 entries (`MRB_STACK_GROWTH`)
- Max stack depth: `MRB_STACK_MAX` (0x40000 - 128)
- Max call depth: `MRB_CALL_LEVEL_MAX` (512, or 128 with ASAN)
Exceeding either limit raises `SystemStackError`.
When the value stack is reallocated, all `REnv` objects and
`mrb_callinfo` stack pointers are adjusted by the delta
(`envadjust` function).
## Call Frames
Each method or block call pushes a `mrb_callinfo` frame:
```text
mrb_callinfo
+-- n:4 positional argument count (0-14, 15 = varargs)
+-- nk:4 keyword argument count (0-14, 15 = varargs)
+-- cci call context info (NONE, DIRECT, SKIP, RESUMED)
+-- vis visibility flags (public/private/protected)
+-- mid method symbol
+-- proc current RProc
+-- blk block argument (RProc*)
+-- stack pointer into value stack
+-- pc program counter (bytecode position)
+-- u.env closure environment (REnv*)
+-- u.target_class receiver's class
```
### Stack Layout Per Frame
```text
ci->stack:
[0] self (receiver)
[1..n] positional arguments
[n+1..] keyword argument pairs (key, value, key, value, ...)
[bidx] block argument
[bidx+1..] local variables and temporaries
```
### Argument Count Encoding
The `n` and `nk` fields are 4 bits each (0-15). When `n == 15`,
positional arguments are packed into a single Array in register 1.
When `nk == 15`, keyword arguments are packed into a single Hash.
The block index is calculated by `mrb_bidx(n, nk)`:
```text
if n == 15: n = 1 (array)
if nk == 15: n += 1 (hash)
else: n += nk * 2 (key-value pairs)
return n + 1 (skip self)
```
### Call Context Info (cci)
| Value | Name | Meaning |
| ----- | --------------- | ------------------------------------- |
| 0 | `CINFO_NONE` | Normal VM-to-VM call |
| 1 | `CINFO_DIRECT` | Explicit VM call (block, lambda.call) |
| 2 | `CINFO_SKIP` | Skip frame in stack traces |
| 3 | `CINFO_RESUMED` | Fiber resumed (stop execution) |
## Dispatch Loop
The main loop in `mrb_vm_run()` decodes and dispatches opcodes.
Two dispatch strategies are available:
- **Computed goto** (default on GCC/Clang): a jump table of label
addresses (`optable[]`) for direct dispatch. Faster due to
better branch prediction.
- **Switch-based** (`MRB_USE_VM_SWITCH_DISPATCH`): a standard
`switch(insn)` statement. Default on MSVC and other compilers.
The dispatch loop is wrapped in `MRB_TRY`/`MRB_CATCH` for exception
handling (see [Exception Handling](#exception-handling)).
## Method Dispatch
When `OP_SEND` (or `OP_SSEND`, `OP_SUPER`) executes:
### 1. Prepare Arguments
Determine argument layout. If argument count < 15, the fast path
uses inline registers. Otherwise, arguments are packed into an
Array (varargs mode).
### 2. Push Call Frame
```c
ci = cipush(mrb, a, CINFO_DIRECT, NULL, NULL, blk, mid, argc);
```
The new frame's stack starts at the previous frame's stack + `a`
(the receiver's register index).
### 3. Method Lookup
The lookup sequence:
1. **Method cache check**: hash table lookup by `(class, mid)`.
Default cache size: `MRB_METHOD_CACHE_SIZE` (256 entries).
2. **Method table walk**: if cache misses, search the receiver's
class method table (`mt`), then walk the superclass chain.
3. **Cache store**: on successful lookup, store in the cache.
The method cache is invalidated when classes are modified
(`mrb_mc_clear_by_class`).
### 4. Invoke
- **Ruby method** (irep-based): extend the stack to `irep->nregs`,
set `ci->pc` to `irep->iseq`, and jump to the new bytecode.
- **C function**: call `func(mrb, recv)` directly, then pop the
call frame and store the return value.
### 5. Visibility Check
Private methods are only callable without an explicit receiver.
Protected methods are callable from the same class hierarchy.
Violations raise `NoMethodError`.
## Exception Handling
### setjmp/longjmp
By default, mruby uses `setjmp`/`longjmp` for exception control
flow:
```c
MRB_TRY(&c_jmp) {
mrb->jmp = &c_jmp;
/* dispatch loop */
}
MRB_CATCH(&c_jmp) {
/* handle exception */
}
MRB_END_EXC(&c_jmp);
```
With `MRB_USE_CXX_EXCEPTION`, C++ `try`/`catch` is used instead.
### Handler Table
Each irep contains a catch handler table (appended after iseq in
memory) with entries for `rescue` and `ensure` blocks:
```text
mrb_irep_catch_handler
+-- type RESCUE (0) or ENSURE (1)
+-- begin[4] start PC of protected range
+-- end[4] end PC of protected range
+-- target[4] jump target when handler matches
```
### Unwinding Process
When an exception occurs:
1. Search the current irep's catch handler table (reverse order)
for a handler covering the current PC
2. If an `ensure` handler is found: execute it (may re-raise)
3. If a `rescue` handler is found: jump to handler code
4. If no handler: pop the call frame (`cipop`) and repeat with
the parent frame
5. `CINFO_DIRECT` frames are destroyed during propagation
## Block and Closure Handling
### REnv (Environment)
Closures capture their enclosing scope's variables through `REnv`:
```text
REnv
+-- stack pointer to captured variable values
+-- cxt owning context (NULL if detached from stack)
+-- mid method symbol
+-- flags length, block index, visibility
```
While the defining scope is active, `REnv::stack` points directly
into the VM value stack (shared). This avoids copying.
### Environment Unsharing
When a closure outlives its defining scope, `mrb_env_unshare()`
copies the captured variables from the stack to a heap-allocated
buffer:
```c
mrb_env_unshare(mrb, env, noraise);
```
After unsharing, `MRB_ENV_CLOSE(env)` sets `cxt = NULL` to indicate
the environment is detached. A write barrier is issued for GC
correctness.
### Proc Types
| Flag | Meaning |
| ------------------- | ------------------------------ |
| `MRB_PROC_CFUNC_FL` | C function (not irep-based) |
| `MRB_PROC_STRICT` | Lambda (strict argument check) |
| `MRB_PROC_ORPHAN` | No environment attachment |
| `MRB_PROC_ENVSET` | Has captured environment |
| `MRB_PROC_SCOPE` | Defines a new variable scope |
## Fiber Switching
Fibers are lightweight coroutines. Each fiber has its own
`mrb_context` with separate value and call info stacks.
### Fiber States
```text
CREATED --> RUNNING --> SUSPENDED --> TERMINATED
| ^
+-----------+
(yield/resume)
TRANSFERRED (via Fiber#transfer)
```
### Context Switch
On `Fiber#resume`:
1. Save current context state
2. Set `mrb->c` to the fiber's context
3. Push arguments onto the fiber's stack
4. Continue execution in the fiber
On `Fiber.yield`:
1. Save fiber context
2. Restore the parent context (`mrb->c = c->prev`)
3. Return yield values to the parent
### Fiber Termination
When a fiber completes (`fiber_terminate`):
1. Unshare any environments that reference the fiber's stack
2. Set status to `TERMINATED`
3. Free the fiber's stacks
4. Switch to the previous context
### C Function Boundary
Fibers cannot yield across C function boundaries. You cannot call
`Fiber.yield` from within a C-implemented method (except via
`mrb_fiber_yield` at return). This is because C call frames cannot
be suspended and resumed.
## GC Integration
The VM saves the arena index at the start of the dispatch loop:
```c
int ai = mrb_gc_arena_save(mrb);
```
After each C function call, the arena is shrunk back:
```c
mrb_gc_arena_shrink(mrb, ai);
```
This prevents temporary objects created by C functions from
accumulating in the arena.
Write barriers are issued when environments are detached or closed,
ensuring the incremental GC correctly tracks live references.
## Source Files
| File | Contents |
| ----------------------- | ---------------------------------------------- |
| `src/vm.c` | Dispatch loop, method invocation (~1900 lines) |
| `include/mruby.h` | `mrb_state`, `mrb_callinfo`, `mrb_context` |
| `include/mruby/proc.h` | `RProc`, `REnv` structures |
| `include/mruby/throw.h` | `MRB_TRY`/`MRB_CATCH` macros |