doc: add ROM method table guide

Comprehensive documentation covering architecture, definition
format, flags, extension gem usage, conditional methods, runtime
behavior (COW, method removal, GC), and conversion guide.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-02-19 14:38:29 +09:00
parent bcc585a80f
commit 16fb8a6b08
+411
View File
@@ -0,0 +1,411 @@
<!-- summary: ROM Method Tables for Memory-Efficient Method Registration -->
# ROM Method Tables
ROM method tables allow C methods to be registered using static data
stored in ROM (read-only memory) rather than heap-allocated RAM. This
saves significant memory on embedded systems where RAM is scarce.
## Motivation
In a default mruby build, `mrb_open()` builds ~40 classes with ~700+
method entries at startup. Each method entry is heap-allocated via
individual `mrb_define_method_id()` calls. On a constrained MCU, this
consumes ~14KB of RAM for method table metadata alone.
ROM method tables eliminate this cost by placing method metadata in
static `const` data at compile time. Only runtime mutations (e.g.,
reopening a class to add methods) trigger heap allocation.
## Architecture
### Chained Layers
Each class has a method table (`mt`) pointer to a linked list of
`mt_tbl` layers:
```
String.mt -> [mutable layer] -> [string_ext ROM] -> [string_core ROM] -> NULL
```
**Lookup** walks the chain front-to-back, returning the first match.
The method cache makes repeated lookups O(1), so the chain walk
only occurs on cache misses.
**Mutation** uses copy-on-write (COW): if the top layer is read-only,
a new mutable layer is created in front of it. The ROM data is never
modified.
```
Before: String.mt -> [string_ext ROM] -> [string_core ROM] -> NULL
After String.define_method(:foo):
String.mt -> [mutable: foo] -> [string_ext ROM] -> [string_core ROM] -> NULL
```
### Memory Layout
Each `mt_tbl` stores method entries as parallel arrays in a single
contiguous block:
```
ptr -> [ vals[0] vals[1] ... vals[N-1] | keys[0] keys[1] ... keys[N-1] ]
|<--- union mt_ptr array ------>|<--- mrb_sym (encoded) array -->|
```
Values are `union mt_ptr` (function pointer or proc pointer). Keys are
`mrb_sym` with flags packed into the lower bits using `MT_KEY()`.
Keys must be sorted by symbol ID for binary search. The
`mrb_mt_init_rom()` function handles sorting at startup, so the
source code order does not matter.
## How to Define a ROM Method Table
### Step 1: Define the Static Data
Include `<mruby/internal.h>` (which provides `mt_tbl`, `union mt_ptr`,
`MT_KEY()`, and flag constants) and define the ROM data structure:
```c
#include <mruby/internal.h>
#include <mruby/presym.h>
#define MY_ROM_MT_SIZE 3
static struct {
union mt_ptr vals[MY_ROM_MT_SIZE];
mrb_sym keys[MY_ROM_MT_SIZE];
} my_rom_data = {
.vals = {
{ .func = my_method_a },
{ .func = my_method_b },
{ .func = my_method_c },
},
.keys = {
MT_KEY(MRB_SYM(method_a), MT_FUNC|MT_PUBLIC),
MT_KEY(MRB_SYM(method_b), MT_FUNC|MT_NOARG|MT_PUBLIC),
MT_KEY(MRB_OPSYM(eq), MT_FUNC|MT_PUBLIC),
}
};
static mt_tbl my_rom_mt = {
MY_ROM_MT_SIZE, MY_ROM_MT_SIZE,
(union mt_ptr*)&my_rom_data, NULL
};
```
### Step 2: Register in the Init Function
Replace `mrb_define_method_id()` calls with a single
`mrb_mt_init_rom()` call:
```c
void
mrb_mruby_mygem_gem_init(mrb_state *mrb)
{
struct RClass *c = mrb_define_class_id(mrb, MRB_SYM(MyClass), mrb->object_class);
mrb_mt_init_rom(c, &my_rom_mt);
}
```
`mrb_mt_init_rom()` sorts the keys by symbol ID, sets the readonly
flag, and pushes the ROM layer onto the class's method table chain.
### Step 3: Verify
Build and run the test suite. ROM tables are semantically transparent
to Ruby code.
## Reference
### Data Types
Defined in `include/mruby/internal.h`:
```c
union mt_ptr {
const struct RProc *proc;
mrb_func_t func;
};
typedef struct mt_tbl {
int size;
int alloc; /* bit 30: MT_READONLY_BIT */
union mt_ptr *ptr;
struct mt_tbl *next; /* next (lower-priority) layer, or NULL */
} mt_tbl;
```
### Key Encoding
```c
#define MT_KEY(sym, flags) ((sym) << MT_KEY_SHIFT | (flags))
```
### Flags
| Flag | Value | Description |
| ------------ | ----- | ---------------------------------------------- |
| `MT_FUNC` | 8 | Entry is a C function pointer (not an RProc) |
| `MT_NOARG` | 4 | Method takes no arguments (optimization hint) |
| `MT_PUBLIC` | 0 | Public visibility |
| `MT_PRIVATE` | 1 | Private visibility |
Most ROM entries use `MT_FUNC|MT_PUBLIC` or `MT_FUNC|MT_NOARG|MT_PUBLIC`.
**How to choose flags:**
- **`MT_FUNC`**: Always set for C function methods. Omit only for
RProc-based methods (rare in ROM tables).
- **`MT_NOARG`**: Set when the original `mrb_define_method_id()` used
`MRB_ARGS_NONE()`. This enables an optimized call path in the VM.
- **`MT_PUBLIC` / `MT_PRIVATE`**: Match the intended visibility. Almost
all methods are public.
### Symbol Macros
Use the presym macros for keys. See `doc/guides/symbol.md` for the
full list:
```c
MRB_SYM(size) /* size */
MRB_SYM_B(chomp) /* chomp! */
MRB_SYM_Q(frozen) /* frozen? */
MRB_SYM_E(name) /* name= */
MRB_OPSYM(add) /* + */
MRB_OPSYM(eq) /* == */
MRB_OPSYM(aref) /* [] */
MRB_OPSYM(aset) /* []= */
MRB_OPSYM(cmp) /* <=> */
MRB_IVSYM(name) /* @name */
```
### API
```c
void mrb_mt_init_rom(struct RClass *c, mt_tbl *rom);
```
Sorts the ROM table, sets the readonly flag, and pushes it onto the
class's method table chain. Multiple calls push additional layers,
which is how extension gems add methods to core classes.
## Vals and Keys Correspondence
Each `vals[i]` corresponds to `keys[i]`. The function pointer in
`vals[i]` is the C implementation of the method identified by
`keys[i]`. Their order in the source code does not matter (they are
sorted at init time), but keeping them in the same order improves
readability.
**Method aliases** (two names for the same function) are expressed as
separate entries sharing the same function pointer:
```c
.vals = {
{ .func = mrb_str_size }, /* size */
{ .func = mrb_str_size }, /* length (alias) */
},
.keys = {
MT_KEY(MRB_SYM(size), MT_FUNC|MT_NOARG|MT_PUBLIC),
MT_KEY(MRB_SYM(length), MT_FUNC|MT_NOARG|MT_PUBLIC),
}
```
## Conditional Methods
Methods that depend on build configuration (e.g., `MRB_NO_FLOAT`) can
be handled in two ways:
**Option A: Separate ROM table under `#ifdef`** (preferred for large
blocks):
```c
#ifndef MRB_NO_FLOAT
#define FLOAT_ROM_MT_SIZE 29
static struct { ... } float_rom_data = { ... };
static mt_tbl float_rom_mt = { ... };
#endif
void mrb_init_numeric(mrb_state *mrb) {
mrb_mt_init_rom(integer, &integer_rom_mt);
#ifndef MRB_NO_FLOAT
mrb_mt_init_rom(fl, &float_rom_mt);
#endif
}
```
**Option B: Keep as `mrb_define_method_id()`** (preferred for a few
conditional methods):
```c
void mrb_init_numeric(mrb_state *mrb) {
mrb_mt_init_rom(integer, &integer_rom_mt);
#ifndef MRB_NO_FLOAT
mrb_define_method_id(mrb, integer, MRB_SYM(to_f), int_to_f, MRB_ARGS_NONE());
#endif
}
```
Both approaches work correctly. The ROM layer and the
`mrb_define_method_id()` calls coexist: method lookup walks the
mutable layer first, then the ROM chain.
## Extension Gems
Extension gems use exactly the same pattern. Since gems are
initialized after core, calling `mrb_mt_init_rom()` pushes the gem's
ROM layer in front of the core ROM layer:
```c
/* mrbgems/mruby-string-ext/src/string.c */
#define STRING_EXT_ROM_MT_SIZE 53
static struct { ... } string_ext_rom_data = { ... };
static mt_tbl string_ext_rom_mt = { ... };
void mrb_mruby_string_ext_gem_init(mrb_state *mrb)
{
struct RClass *s = mrb->string_class;
mrb_mt_init_rom(s, &string_ext_rom_mt);
}
```
After initialization, String's method table chain looks like:
```
String.mt -> [string_ext ROM, 53 methods]
-> [string_core ROM, 46 methods]
-> NULL
```
A gem may also define ROM tables for multiple classes:
```c
void mrb_mruby_mygem_gem_init(mrb_state *mrb)
{
mrb_mt_init_rom(mrb->string_class, &string_mygem_rom_mt);
mrb_mt_init_rom(mrb->integer_class, &integer_mygem_rom_mt);
}
```
## Methods That Cannot Use ROM Tables
Some methods must remain as `mrb_define_method_id()` calls:
- **Class methods** (`mrb_define_class_method_id()`): ROM tables
register instance methods only.
- **Module functions** (`mrb_define_module_function_id()`): Same
reason.
- **Methods requiring `mrb_state*` at definition time**: For example,
methods that create frozen RProc objects during init.
- **Methods on dynamically created classes**: Classes created at
init time (not stored in `mrb->xxx_class`) that require
`mrb_define_class()` to obtain the class pointer.
These methods are added after `mrb_mt_init_rom()` and go into the
mutable layer that sits in front of the ROM chain.
## Runtime Behavior
### Open Classes (COW)
Ruby's open classes work transparently. When a Ruby program or C code
adds a method to a class with a ROM table, the COW mechanism creates a
mutable layer:
```ruby
class String
def my_custom_method
42
end
end
"hello".my_custom_method #=> 42
"hello".size #=> 5 (still found in ROM layer)
```
### Method Removal
`remove_method` and `undef_method` work on ROM methods. If the target
method is only in a ROM layer, the chain is flattened into a single
mutable table first, then the entry is deleted. This is an O(n)
operation but is extremely rare for built-in methods.
### Class Duplication
`Class.dup` shares the ROM chain. The duplicated class gets an empty
mutable layer pointing to the same ROM layers as the original. No ROM
data is copied.
### Garbage Collection
ROM layers are skipped during GC mark and sweep phases. Only mutable
layers are scanned for live RProc references and freed when the class
is collected. This reduces GC overhead.
### Memory Measurement
`mrb_class_mt_memsize()` reports only mutable layer memory. ROM layers
are not counted since they do not consume heap memory.
## Converting Existing Code
To convert existing `mrb_define_method_id()` calls to a ROM table:
1. **Count** the number of method definitions that can be converted.
2. **Create** the ROM data structure with `#define MY_ROM_MT_SIZE N`.
3. **Move** each `mrb_define_method_id()` call into the ROM table:
- The second-to-last argument (function pointer) goes into `.vals`.
- The third argument (symbol) goes into `.keys` via `MT_KEY()`.
- Map the `MRB_ARGS_*` macro to flags:
- `MRB_ARGS_NONE()` -> `MT_FUNC|MT_NOARG|MT_PUBLIC`
- Anything else -> `MT_FUNC|MT_PUBLIC`
4. **Replace** the calls with `mrb_mt_init_rom(c, &my_rom_mt)`.
5. **Keep** any methods that cannot be converted (see above) as
individual `mrb_define_method_id()` calls after the ROM init.
6. **Build and test**: `rake CONFIG=host-debug -j24 all test:run:serial`
### Before
```c
void mrb_mruby_foo_gem_init(mrb_state *mrb) {
struct RClass *foo = mrb_define_class_id(mrb, MRB_SYM(Foo), mrb->object_class);
mrb_define_method_id(mrb, foo, MRB_SYM(bar), foo_bar, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, foo, MRB_SYM(baz), foo_baz, MRB_ARGS_NONE());
mrb_define_method_id(mrb, foo, MRB_OPSYM(eq), foo_eq, MRB_ARGS_REQ(1));
}
```
### After
```c
#define FOO_ROM_MT_SIZE 3
static struct {
union mt_ptr vals[FOO_ROM_MT_SIZE];
mrb_sym keys[FOO_ROM_MT_SIZE];
} foo_rom_data = {
.vals = {
{ .func = foo_bar },
{ .func = foo_baz },
{ .func = foo_eq },
},
.keys = {
MT_KEY(MRB_SYM(bar), MT_FUNC|MT_PUBLIC),
MT_KEY(MRB_SYM(baz), MT_FUNC|MT_NOARG|MT_PUBLIC),
MT_KEY(MRB_OPSYM(eq), MT_FUNC|MT_PUBLIC),
}
};
static mt_tbl foo_rom_mt = {
FOO_ROM_MT_SIZE, FOO_ROM_MT_SIZE,
(union mt_ptr*)&foo_rom_data, NULL
};
void mrb_mruby_foo_gem_init(mrb_state *mrb) {
struct RClass *foo = mrb_define_class_id(mrb, MRB_SYM(Foo), mrb->object_class);
mrb_mt_init_rom(foo, &foo_rom_mt);
}
```