class.c: add MRB_MT_FROZEN_BIT for method table layers

When mrb_mt_init_rom() is called on a class that already has a
mutable method table (from prior mrb_define_method_id() calls),
the mutable top layer is now frozen in place instead of being
left as a writable layer that wastes RAM on embedded systems.

The frozen bit (bit 29 of alloc field) marks heap-allocated
method table layers as temporarily immutable. Unlike the
readonly bit (bit 30, for true ROM), frozen layers are
automatically unfrozen when methods are later added via
mrb_define_method_raw() or removed via mrb_remove_method().
This preserves the c->mt pointer, which is critical because
iclasses (from module inclusion) hold a copy of it.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-02-19 23:25:16 +09:00
parent a208440c4f
commit cfd9214b3d
2 changed files with 19 additions and 4 deletions
+2 -1
View File
@@ -36,12 +36,13 @@ union mrb_mt_ptr {
typedef struct mrb_mt_tbl {
int size;
int alloc; /* bit 30: MRB_MT_READONLY_BIT */
int alloc; /* bit 30: MRB_MT_READONLY_BIT, bit 29: MRB_MT_FROZEN_BIT */
union mrb_mt_ptr *ptr;
struct mrb_mt_tbl *next;
} mrb_mt_tbl;
#define MRB_MT_READONLY_BIT (1 << 30)
#define MRB_MT_FROZEN_BIT (1 << 29)
#define MRB_MT_KEY_SHIFT 4
#define MRB_MT_KEY(sym, flags) ((sym)<<MRB_MT_KEY_SHIFT|(flags))
#define MRB_MT_FUNC 8 /* MRB_METHOD_FUNC_FL */
+17 -3
View File
@@ -33,8 +33,10 @@
#define MT_KEY_SYM(k) ((k)>>MRB_MT_KEY_SHIFT)
#define MT_KEY_FLG(k) ((k)&MT_KEY_MASK)
#define MT_ALLOC(t) ((t)->alloc & ~MRB_MT_READONLY_BIT)
#define MRB_MT_FLAG_BITS (MRB_MT_READONLY_BIT | MRB_MT_FROZEN_BIT)
#define MT_ALLOC(t) ((t)->alloc & ~MRB_MT_FLAG_BITS)
#define mt_readonly_p(t) ((t)->alloc & MRB_MT_READONLY_BIT)
#define mt_frozen_p(t) ((t)->alloc & MRB_MT_FROZEN_BIT)
/* Helper to get keys array from method table */
static inline mrb_sym*
@@ -65,7 +67,7 @@ mt_grow(mrb_state *mrb, mrb_mt_tbl *t, int new_alloc)
/* move the old key array up to its new position */
memmove(new_keys, old_keys, old_alloc * sizeof(mrb_sym));
}
t->alloc = new_alloc;
t->alloc = (t->alloc & MRB_MT_FLAG_BITS) | new_alloc;
}
/* Creates a new empty method table */
@@ -285,6 +287,10 @@ mrb_mt_init_rom(struct RClass *c, mrb_mt_tbl *rom)
c->mt = rom;
}
else {
/* freeze mutable top, insert ROM behind it;
* c->mt must not change because iclasses (module inclusion)
* hold a copy of the mt pointer */
t->alloc |= MRB_MT_FROZEN_BIT;
rom->next = t->next;
t->next = rom;
}
@@ -1090,6 +1096,11 @@ mrb_define_method_raw(mrb_state *mrb, struct RClass *c, mrb_sym mid, mrb_method_
if (!h) {
h = c->mt = mt_new(mrb);
}
else if (mt_frozen_p(h)) {
/* unfreeze heap-allocated frozen layer to preserve c->mt pointer
* (iclasses hold a copy of the mt pointer for included modules) */
h->alloc &= ~MRB_MT_FROZEN_BIT;
}
else if (mt_readonly_p(h)) {
/* COW: create mutable top layer, chain to ROM */
mrb_mt_tbl *top = mt_new(mrb);
@@ -3654,7 +3665,10 @@ mrb_remove_method(mrb_state *mrb, struct RClass *c0, mrb_sym mid)
union mrb_mt_ptr tombstone;
tombstone.func = NULL;
found = TRUE;
if (mt_readonly_p(h)) {
if (mt_frozen_p(h)) {
h->alloc &= ~MRB_MT_FROZEN_BIT;
}
else if (mt_readonly_p(h)) {
mrb_mt_tbl *top = mt_new(mrb);
top->next = h;
h = c->mt = top;