Major port of Lua 5.1 incremental GC patterns: Tri-color system: - Two-white (WHITE0/WHITE1) with currentwhite flip in atomic phase - Objects born currentwhite via hooked luaC_link (0x6F7B20) - String birth mark patched to currentwhite at each flip - isDead: otherwhite check with FIXED exception for WoW - makewhite on survivors (preserves KEYWEAK/VALUEWEAK/FIXED) - Bootstrap: first cycle sets all existing objects to currentwhite Mark system (5.1 reallymarkobject/propagatemark): - Strings: stringmark inline (no gray stack) - Userdata: go black inline, mark metatable - Tables: gray2black, traverse, black2gray if weak - Threads: NEVER black, always grayagain (re-traversed in atomic) - Closures: go black normally Forward barriers (5.1 luaC_barrierf at every write site): - OP_SETUPVAL: VM patch at 0x6F8A97 with naked asm trampoline - lua_setmetatable (0x6F4020): hooked, barrier on new metatable - lua_setupvalue (0x6F47B0): hooked, barrier grays closure - lua_setfenv (0x6F40D0): hooked, barrier grays object Backward barriers (5.1 luaC_barrierback): - lua_table_set_value (0x6FA840): black2gray + grayagain - lua_table_set_int_key (0x6FAD80): black2gray + grayagain Atomic phase (5.1 ordering): - propagateall, re-traverse weak tables, mark running thread, grayagain drain, separateudata, marktmu, cleartable, flip white Status: enters world successfully with CHUNK_SIZE=5000 (incremental). Crashes on UI reload (different crash pattern -- NULL+8 deref in mark, not the old hash chain crash). Progress from "crashes on enter world" to "crashes on UI reload."
1761 lines
65 KiB
Zig
1761 lines
65 KiB
Zig
//! From-scratch incremental GC for Lua 5.0 (WoW 1.12.1 build 5875).
|
|
//!
|
|
//! Replaces WoW's stop-the-world mark+sweep with a chunked incremental GC.
|
|
//! Each call to luaC_collectgarbage processes one chunk of work (~5000 objects,
|
|
//! target <2ms) then yields. A state machine tracks progress across calls:
|
|
//!
|
|
//! idle -> marking -> atomic -> sweeping -> sweep_strings -> finalize -> idle
|
|
//!
|
|
//! The module also owns allocation via a slab allocator (luaalloc.zig).
|
|
//!
|
|
//! Only two native GC functions are called:
|
|
//! - lua_gc_free_object (0x6F7260): type-dispatch free
|
|
//! - luaCallUserDataGC (0x6F7080): runs __gc metamethods
|
|
//! - luaC_separateudata (0x6F6FF0): moves dead __gc udata to tmudata
|
|
//!
|
|
//! Everything else (mark, sweep, string sweep, threshold, marktmu, weak table
|
|
//! clearing) is implemented from scratch.
|
|
//!
|
|
//! ## Tri-color incremental mark
|
|
//! WHITE = marked bit 0 (unprocessed)
|
|
//! GRAY = in gray stack (pending traversal)
|
|
//! BLACK = marked bit 1 (processed, children traversed)
|
|
//!
|
|
//! Write barriers on table writes push BLACK tables back to gray during mark.
|
|
|
|
const std = @import("std");
|
|
const offsets = @import("lua_offsets.zig");
|
|
const hook = @import("zhook");
|
|
const lua = @import("../lua.zig");
|
|
const logging = @import("../logging.zig");
|
|
const mod_mutex = @import("../mutex.zig");
|
|
const luaalloc = @import("luaalloc.zig");
|
|
|
|
pub const module_name: [*:0]const u8 = "luagc";
|
|
|
|
var g_mutex: ?*anyopaque = null;
|
|
var g_is_hook_owner: bool = false;
|
|
var log: logging.Logger = .{};
|
|
|
|
// =============================================================================
|
|
// Runtime stats (exposed to Lua via ZGCStats() global function)
|
|
// =============================================================================
|
|
|
|
const Stats = struct {
|
|
cycles_total: u32 = 0,
|
|
mark_steps_last: u32 = 0,
|
|
sweep_steps_last: u32 = 0,
|
|
gray_peak: u32 = 0,
|
|
dead_freed_last: u32 = 0,
|
|
strings_freed_last: u32 = 0,
|
|
};
|
|
|
|
var stats: Stats = .{};
|
|
|
|
// In-progress counters copied to Stats at cycle end.
|
|
var cur_mark_steps: u32 = 0;
|
|
var cur_sweep_steps: u32 = 0;
|
|
var cur_dead_freed: u32 = 0;
|
|
var cur_strings_freed: u32 = 0;
|
|
|
|
// =============================================================================
|
|
// GC State Machine
|
|
// =============================================================================
|
|
|
|
const Phase = enum(u8) {
|
|
/// No collection in progress. Next trigger starts a new cycle.
|
|
idle = 0,
|
|
/// Incremental mark: draining gray stack in chunks.
|
|
marking = 1,
|
|
/// Atomic phase: weak table clear, separateudata, marktmu, final propagate.
|
|
/// Runs to completion in a single call (bounded by tmudata size).
|
|
atomic = 2,
|
|
/// Incremental sweep of rootgc list.
|
|
sweeping = 3,
|
|
/// Incremental sweep of string hash buckets.
|
|
sweep_strings = 4,
|
|
/// Atomic: run __gc finalizers + set threshold. Short.
|
|
finalize = 5,
|
|
};
|
|
|
|
var phase: Phase = .idle;
|
|
var saved_L: u32 = 0;
|
|
var saved_g: u32 = 0;
|
|
|
|
/// Objects to process per step. High value = atomic (working). Lower for
|
|
/// incremental once the multi-step crash is resolved.
|
|
const CHUNK_SIZE: u32 = 1000000;
|
|
|
|
// =============================================================================
|
|
// Gray Stack
|
|
// =============================================================================
|
|
|
|
const GRAY_STACK_SIZE: u32 = 524288; // 2MB, covers 200k object heap
|
|
var gray_stack: [GRAY_STACK_SIZE]u32 = undefined;
|
|
var gray_count: u32 = 0;
|
|
|
|
fn grayStackPush(obj: u32) void {
|
|
if (gray_count < GRAY_STACK_SIZE) {
|
|
gray_stack[gray_count] = obj;
|
|
gray_count += 1;
|
|
if (gray_count > stats.gray_peak) stats.gray_peak = gray_count;
|
|
}
|
|
}
|
|
|
|
fn grayStackPop() u32 {
|
|
if (gray_count == 0) return 0;
|
|
gray_count -= 1;
|
|
return gray_stack[gray_count];
|
|
}
|
|
|
|
// =============================================================================
|
|
// Gray-Again List (barrier back)
|
|
// =============================================================================
|
|
//
|
|
// During incremental mark, the write barrier does NOT push tables back to the
|
|
// main gray stack (that causes livelock under heavy allocation). Instead,
|
|
// written-to BLACK tables go on the grayagain list. They're re-traversed once
|
|
// during the atomic phase, catching all mutations since their last traversal.
|
|
// This matches Lua 5.1's luaC_barrierback / grayagain pattern.
|
|
|
|
const GRAYAGAIN_SIZE: u32 = 65536;
|
|
var grayagain_list: [GRAYAGAIN_SIZE]u32 = undefined;
|
|
var grayagain_count: u32 = 0;
|
|
|
|
fn grayagainAdd(table: u32) void {
|
|
if (grayagain_count < GRAYAGAIN_SIZE) {
|
|
grayagain_list[grayagain_count] = table;
|
|
grayagain_count += 1;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Incremental Sweep Cursors
|
|
// =============================================================================
|
|
|
|
/// For rootgc sweep: pointer to the "next" field that points to current obj.
|
|
/// This is &(prev->next) or &(g->rootgc). Allows in-place unlinking.
|
|
var sweep_prev_next: u32 = 0;
|
|
|
|
/// For string sweep: current bucket index.
|
|
var sweep_string_bucket: u32 = 0;
|
|
|
|
// =============================================================================
|
|
// Weak Table Tracking
|
|
// =============================================================================
|
|
|
|
const WEAK_TABLE_LIST_SIZE: u32 = 4096;
|
|
var weak_table_list: [WEAK_TABLE_LIST_SIZE]u32 = undefined;
|
|
var weak_table_count: u32 = 0;
|
|
|
|
fn weakTableListAdd(table: u32) void {
|
|
if (weak_table_count < WEAK_TABLE_LIST_SIZE) {
|
|
weak_table_list[weak_table_count] = table;
|
|
weak_table_count += 1;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Bootstrap
|
|
// =============================================================================
|
|
|
|
/// First cycle flag: existing objects from native GC have marked=0 (neither
|
|
/// white). On the first cycle, walk all objects and set them to currentwhite
|
|
/// so our mark can find them.
|
|
var needs_bootstrap: bool = true;
|
|
|
|
/// Update the string creation birth mark byte to match currentwhite.
|
|
/// Strings bypass luaC_link and have their own `MOV byte [EBX+5], 0x00`
|
|
/// at 0x6F9DC1. We patch this immediate to currentwhite.
|
|
fn syncStringBirthMark() void {
|
|
var patch = [1]u8{currentwhite};
|
|
hook.writeProtected(offsets.birth_mark_string, &patch);
|
|
}
|
|
|
|
fn bootstrapWhite(g: u32) void {
|
|
// Walk rootgc and set all objects to currentwhite
|
|
var obj = readU32(g + offsets.GS_rootgc);
|
|
while (obj != 0) {
|
|
writeU8(obj + offsets.OBJ_marked, currentwhite);
|
|
obj = readU32(obj + offsets.OBJ_next);
|
|
}
|
|
// Walk rootudata
|
|
obj = readU32(g + offsets.GS_rootudata);
|
|
while (obj != 0) {
|
|
writeU8(obj + offsets.OBJ_marked, currentwhite);
|
|
obj = readU32(obj + offsets.OBJ_next);
|
|
}
|
|
// Walk string hash table
|
|
const hash_array = readU32(g + offsets.GS_strt_hash);
|
|
const bucket_count = readU32(g + offsets.GS_strt_size);
|
|
if (hash_array != 0) {
|
|
var bi: u32 = 0;
|
|
while (bi < bucket_count) : (bi += 1) {
|
|
var s = readU32(hash_array + bi * 4);
|
|
while (s != 0) {
|
|
// Preserve FIXED bit (bit 4), set currentwhite
|
|
const old = readU8(s + offsets.OBJ_marked);
|
|
writeU8(s + offsets.OBJ_marked, (old & (1 << offsets.FIXEDBIT)) | currentwhite);
|
|
s = readU32(s + offsets.OBJ_next);
|
|
}
|
|
}
|
|
}
|
|
// Mainthread (not in rootgc in WoW)
|
|
const mt = readU32(g + offsets.GS_mainthread);
|
|
if (mt != 0) writeU8(mt + offsets.OBJ_marked, currentwhite);
|
|
|
|
needs_bootstrap = false;
|
|
log.print("bootstrap: set all objects to currentwhite\n");
|
|
}
|
|
|
|
// =============================================================================
|
|
// Re-entrance Guard
|
|
// =============================================================================
|
|
|
|
var in_gc: bool = false;
|
|
|
|
// =============================================================================
|
|
// Forward declarations of native functions
|
|
// =============================================================================
|
|
|
|
const lua_gc_free_object_fn = *const fn (u32, u32) callconv(hook.cc.fastcall) void;
|
|
const luaCallUserDataGC_fn = *const fn (u32) callconv(hook.cc.fastcall) void;
|
|
const luaC_separateudata_fn = *const fn (u32) callconv(hook.cc.fastcall) void;
|
|
const lua_string_hash_resize_fn = *const fn (u32, u32) callconv(hook.cc.fastcall) void;
|
|
|
|
const native_mark_fn = *const fn (u32) callconv(hook.cc.fastcall) u32; // mark() returns deadmem
|
|
const native_sweep_fn = *const fn (u32, u32) callconv(hook.cc.fastcall) void; // luaC_sweep(L, all)
|
|
const native_checkSizes_fn = *const fn (u32) callconv(hook.cc.fastcall) void; // checkSizes wraps shrink
|
|
|
|
const native_free_object: lua_gc_free_object_fn = @ptrFromInt(offsets.lua_gc_free_object);
|
|
const native_callGCTM: luaCallUserDataGC_fn = @ptrFromInt(offsets.luaCallUserDataGC);
|
|
const native_separateudata: luaC_separateudata_fn = @ptrFromInt(offsets.luaC_separateudata);
|
|
const native_string_hash_resize: lua_string_hash_resize_fn = @ptrFromInt(offsets.lua_string_hash_resize);
|
|
const native_mark: native_mark_fn = @ptrFromInt(offsets.lua_gc_full_collection);
|
|
const native_shrink_memory: native_checkSizes_fn = @ptrFromInt(offsets.lua_gc_shrink_memory);
|
|
|
|
// =============================================================================
|
|
// Utility
|
|
// =============================================================================
|
|
|
|
inline fn readU32(addr: u32) u32 {
|
|
return @as(*const u32, @ptrFromInt(addr)).*;
|
|
}
|
|
|
|
inline fn writeU32(addr: u32, val: u32) void {
|
|
@as(*u32, @ptrFromInt(addr)).* = val;
|
|
}
|
|
|
|
inline fn readU8(addr: u32) u8 {
|
|
return @as(*const u8, @ptrFromInt(addr)).*;
|
|
}
|
|
|
|
inline fn writeU8(addr: u32, val: u8) void {
|
|
@as(*u8, @ptrFromInt(addr)).* = val;
|
|
}
|
|
|
|
fn getGlobalState(L: u32) u32 {
|
|
return readU32(L + offsets.L_global);
|
|
}
|
|
|
|
inline fn objType(obj: u32) u8 {
|
|
return readU8(obj + offsets.OBJ_tt);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tri-color system (Lua 5.1 lgc.h port)
|
|
// =============================================================================
|
|
//
|
|
// Colors:
|
|
// WHITE = has a white bit set (WHITE0 or WHITE1). Unprocessed / new.
|
|
// GRAY = no white bits, no black bit. Found but children not yet processed.
|
|
// BLACK = bit 2 set. Fully traversed.
|
|
//
|
|
// Two whites: currentwhite alternates between WHITE0 (0x01) and WHITE1 (0x02)
|
|
// each cycle. New objects are born with currentwhite. Sweep kills objects
|
|
// with "otherwhite" (the previous cycle's white). Objects with currentwhite
|
|
// (born during this cycle) or BLACK survive.
|
|
//
|
|
// This eliminates birth mark entirely: objects born during sweep have
|
|
// currentwhite which is NOT otherwhite, so they survive automatically.
|
|
|
|
/// Current white color bit. Alternates between 0x01 (WHITE0) and 0x02 (WHITE1).
|
|
var currentwhite: u8 = 0x01;
|
|
|
|
/// The dead color for sweep: whichever white ISN'T current.
|
|
inline fn otherwhite() u8 {
|
|
return currentwhite ^ offsets.WHITEBITS;
|
|
}
|
|
|
|
/// Is the object white (has either white bit set)?
|
|
inline fn isWhite(obj: u32) bool {
|
|
return (readU8(obj + offsets.OBJ_marked) & offsets.WHITEBITS) != 0;
|
|
}
|
|
|
|
/// Is the object black (bit 2 set)?
|
|
inline fn isBlack(obj: u32) bool {
|
|
return (readU8(obj + offsets.OBJ_marked) & (1 << offsets.BLACKBIT)) != 0;
|
|
}
|
|
|
|
/// Is the object dead? Has otherwhite set AND is not fixed.
|
|
/// Base check from 5.1: `(marked & otherwhite(g) & WHITEBITS)`.
|
|
/// WoW extension: FIXED objects (bit 4, luaS_fix) are never dead -- they
|
|
/// aren't traversed as roots but must survive (reserved words, tmnames).
|
|
inline fn isDead(obj: u32) bool {
|
|
const marked = readU8(obj + offsets.OBJ_marked);
|
|
if ((marked & (1 << offsets.FIXEDBIT)) != 0) return false;
|
|
return (marked & otherwhite() & offsets.WHITEBITS) != 0;
|
|
}
|
|
|
|
/// WHITE -> GRAY: clear both white bits. Object is now gray (pending traversal).
|
|
inline fn white2gray(obj: u32) void {
|
|
writeU8(obj + offsets.OBJ_marked, readU8(obj + offsets.OBJ_marked) & ~offsets.WHITEBITS);
|
|
}
|
|
|
|
/// GRAY -> BLACK: set the black bit.
|
|
inline fn gray2black(obj: u32) void {
|
|
writeU8(obj + offsets.OBJ_marked, readU8(obj + offsets.OBJ_marked) | (1 << offsets.BLACKBIT));
|
|
}
|
|
|
|
/// BLACK -> GRAY: clear the black bit. Used for barrier-back (table written to).
|
|
inline fn black2gray(obj: u32) void {
|
|
writeU8(obj + offsets.OBJ_marked, readU8(obj + offsets.OBJ_marked) & ~@as(u8, 1 << offsets.BLACKBIT));
|
|
}
|
|
|
|
/// Set object to current white, clearing all color bits first.
|
|
/// Preserves non-color flags (KEYWEAK, VALUEWEAK, FIXED, FINALIZED).
|
|
/// Used by sweep on surviving objects.
|
|
inline fn makewhite(obj: u32) void {
|
|
writeU8(obj + offsets.OBJ_marked, (readU8(obj + offsets.OBJ_marked) & offsets.MASKMARKS) | currentwhite);
|
|
}
|
|
|
|
/// Mark an object: matches Lua 5.1 reallymarkobject (lgc.c:69-112).
|
|
/// Strings just get stringmarked (no gray). Userdata go black immediately.
|
|
/// Tables, closures, threads, protos go to gray stack.
|
|
fn markObject(obj: u32) bool {
|
|
if (!isWhite(obj)) return false;
|
|
const tt = objType(obj);
|
|
switch (tt) {
|
|
offsets.LUA_TSTRING => {
|
|
// Strings: just clear white bits, don't go to gray (no children)
|
|
stringmark(obj);
|
|
return true;
|
|
},
|
|
offsets.LUA_TUSERDATA => {
|
|
// Userdata: go black immediately, mark metatable inline
|
|
white2gray(obj);
|
|
gray2black(obj);
|
|
const mt = readU32(obj + offsets.UDATA_metatable);
|
|
if (mt != 0) _ = markObject(mt);
|
|
return true;
|
|
},
|
|
else => {
|
|
// Tables, closures, threads, protos: go to gray stack
|
|
white2gray(obj);
|
|
grayStackPush(obj);
|
|
return true;
|
|
},
|
|
}
|
|
}
|
|
|
|
/// For strings: mark by clearing white bits (stringmark). Strings don't
|
|
/// go to gray (no children to traverse).
|
|
inline fn stringmark(s: u32) void {
|
|
writeU8(s + offsets.OBJ_marked, readU8(s + offsets.OBJ_marked) & ~offsets.WHITEBITS);
|
|
}
|
|
|
|
fn fmt(buf: []u8, comptime f: []const u8, args: anytype) []const u8 {
|
|
return std.fmt.bufPrint(buf, f, args) catch "???";
|
|
}
|
|
|
|
// =============================================================================
|
|
// Mark: Traverse One Object's Children
|
|
// =============================================================================
|
|
|
|
// Debug counters per type (reset each cycle)
|
|
var dbg_table_count: u32 = 0;
|
|
var dbg_closure_count: u32 = 0;
|
|
var dbg_thread_count: u32 = 0;
|
|
var dbg_proto_count: u32 = 0;
|
|
var dbg_udata_count: u32 = 0;
|
|
var dbg_other_count: u32 = 0;
|
|
|
|
/// Pop a gray object, turn it black, and traverse its children.
|
|
/// Matches Lua 5.1 propagatemark (lgc.c:277-320).
|
|
/// THREADS: never go black. They go to grayagain and stay gray.
|
|
/// WEAK TABLES: go back to gray after traversal (black2gray).
|
|
fn propagatemark(obj: u32) void {
|
|
const tt = objType(obj);
|
|
switch (tt) {
|
|
offsets.LUA_TTABLE => {
|
|
dbg_table_count += 1;
|
|
gray2black(obj);
|
|
const is_weak = traverseTable(obj);
|
|
if (is_weak) black2gray(obj); // weak tables stay gray
|
|
},
|
|
offsets.LUA_TFUNCTION => {
|
|
dbg_closure_count += 1;
|
|
gray2black(obj);
|
|
traverseClosure(obj);
|
|
},
|
|
offsets.LUA_TTHREAD => {
|
|
// Lua 5.1: threads NEVER go black. After traversal, add to
|
|
// grayagain so the atomic phase re-traverses the stack.
|
|
dbg_thread_count += 1;
|
|
traverseThread(obj);
|
|
grayagainAdd(obj); // stays gray, re-traversed in atomic
|
|
},
|
|
offsets.LUA_TPROTO => {
|
|
dbg_proto_count += 1;
|
|
gray2black(obj);
|
|
traverseProto(obj);
|
|
},
|
|
offsets.LUA_TUSERDATA => {
|
|
// Should not reach here -- userdata goes black in markObject.
|
|
// But handle gracefully.
|
|
dbg_udata_count += 1;
|
|
gray2black(obj);
|
|
},
|
|
else => {
|
|
dbg_other_count += 1;
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Detect __mode on a table's metatable and set KEYWEAK/VALUEWEAK bits.
|
|
/// Matches native traversetable behavior (lgc.c:157-170).
|
|
///
|
|
/// metatable->flags byte (Table+0x06) caches TM presence: if bit 3 (TM_MODE)
|
|
/// is set, __mode definitely doesn't exist. If not set, we scan the metatable's
|
|
/// hash nodes for a string key matching "__mode".
|
|
///
|
|
/// TString layout: content at +0x10, length at +0x0C.
|
|
fn detectAndSetWeakFlags(table: u32, mt: u32) void {
|
|
// Fast path: check metatable flags cache for TM_MODE (bit 3)
|
|
const mt_flags = readU8(mt + offsets.TABLE_flags);
|
|
if ((mt_flags & 0x08) != 0) return; // TM_MODE cached as absent
|
|
|
|
// Scan metatable hash nodes for __mode key
|
|
const mt_node_ptr = readU32(mt + offsets.TABLE_node);
|
|
if (mt_node_ptr == 0) return;
|
|
const mt_lsize = readU8(mt + offsets.TABLE_lsizenode);
|
|
const mt_sizenode: u32 = @as(u32, 1) << @as(u5, @intCast(mt_lsize));
|
|
|
|
var mode_str: u32 = 0; // the TString* of the __mode value
|
|
var ni: u32 = 0;
|
|
while (ni < mt_sizenode) : (ni += 1) {
|
|
const node = mt_node_ptr + ni * offsets.NODE_size;
|
|
// Key must be a string (type 4)
|
|
if (readU8(node + offsets.NODE_key_tt) != offsets.LUA_TSTRING) continue;
|
|
// Value must be non-nil
|
|
if (readU8(node + offsets.NODE_value_tt) == 0) continue;
|
|
|
|
const key_str = readU32(node + offsets.NODE_key_gcptr);
|
|
if (key_str == 0) continue;
|
|
|
|
// Check string content: length must be 6 ("__mode")
|
|
const len = readU32(key_str + 0x0C);
|
|
if (len != 6) continue;
|
|
|
|
// Compare content at TString+0x10
|
|
const data: [*]const u8 = @ptrFromInt(key_str + 0x10);
|
|
if (data[0] == '_' and data[1] == '_' and data[2] == 'm' and
|
|
data[3] == 'o' and data[4] == 'd' and data[5] == 'e')
|
|
{
|
|
// Found __mode. Value must be a string.
|
|
if (readU8(node + offsets.NODE_value_tt) != offsets.LUA_TSTRING) break;
|
|
mode_str = readU32(node + offsets.NODE_value_gcptr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Clear old weak bits, then set based on __mode string content
|
|
var marked = readU8(table + offsets.OBJ_marked);
|
|
marked &= ~(offsets.KEYWEAK | offsets.VALUEWEAK);
|
|
|
|
if (mode_str != 0) {
|
|
const mode_len = readU32(mode_str + 0x0C);
|
|
const mode_data: [*]const u8 = @ptrFromInt(mode_str + 0x10);
|
|
var weakkey = false;
|
|
var weakvalue = false;
|
|
var mi: u32 = 0;
|
|
while (mi < mode_len) : (mi += 1) {
|
|
if (mode_data[mi] == 'k') weakkey = true;
|
|
if (mode_data[mi] == 'v') weakvalue = true;
|
|
}
|
|
if (weakkey) marked |= offsets.KEYWEAK;
|
|
if (weakvalue) marked |= offsets.VALUEWEAK;
|
|
}
|
|
|
|
writeU8(table + offsets.OBJ_marked, marked);
|
|
}
|
|
|
|
/// Traverse a table's children. Returns true if the table is weak.
|
|
/// Matches Lua 5.1 traversetable (lgc.c:158-196).
|
|
fn traverseTable(table: u32) bool {
|
|
const mt = readU32(table + offsets.TABLE_metatable);
|
|
if (mt != 0) _ = markObject(mt);
|
|
|
|
if (mt != 0) detectAndSetWeakFlags(table, mt);
|
|
|
|
const marked = readU8(table + offsets.OBJ_marked);
|
|
const weakkey = (marked & offsets.KEYWEAK) != 0;
|
|
const weakvalue = (marked & offsets.VALUEWEAK) != 0;
|
|
const is_weak = weakkey or weakvalue;
|
|
|
|
// 5.1: if both-weak, return early (only metatable marked)
|
|
if (weakkey and weakvalue) {
|
|
if (is_weak) weakTableListAdd(table);
|
|
return true;
|
|
}
|
|
|
|
// Array: skip if value-weak
|
|
if (!weakvalue) {
|
|
const array_ptr = readU32(table + offsets.TABLE_array);
|
|
const sizearray = readU32(table + offsets.TABLE_sizearray);
|
|
if (array_ptr != 0) {
|
|
var i: u32 = 0;
|
|
while (i < sizearray) : (i += 1) {
|
|
const tv = array_ptr + i * offsets.TVALUE_size;
|
|
const tt = readU8(tv + offsets.TVALUE_tt);
|
|
if (tt >= offsets.LUA_TSTRING) {
|
|
const gc = readU32(tv + offsets.TVALUE_gcptr);
|
|
if (gc != 0) _ = markObject(gc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hash: condmarkobject(key, !weakkey); condmarkobject(value, !weakvalue)
|
|
const node_ptr = readU32(table + offsets.TABLE_node);
|
|
if (node_ptr == 0) {
|
|
if (is_weak) weakTableListAdd(table);
|
|
return is_weak;
|
|
}
|
|
const lsizenode = readU8(table + offsets.TABLE_lsizenode);
|
|
const sizenode: u32 = @as(u32, 1) << @as(u5, @intCast(lsizenode));
|
|
|
|
var ni: u32 = 0;
|
|
while (ni < sizenode) : (ni += 1) {
|
|
const node = node_ptr + ni * offsets.NODE_size;
|
|
const val_tt = readU8(node + offsets.NODE_value_tt);
|
|
if (val_tt == 0) {
|
|
// 5.1: remove dead entries during traversal (removeentry)
|
|
const key_tt = readU8(node + offsets.NODE_key_tt);
|
|
if (key_tt >= offsets.LUA_TSTRING) {
|
|
writeU8(node + offsets.NODE_key_tt, offsets.LUA_TNONE);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!weakkey) {
|
|
const key_tt = readU8(node + offsets.NODE_key_tt);
|
|
if (key_tt >= offsets.LUA_TSTRING) {
|
|
const key_gc = readU32(node + offsets.NODE_key_gcptr);
|
|
if (key_gc != 0) _ = markObject(key_gc);
|
|
}
|
|
}
|
|
|
|
if (!weakvalue) {
|
|
if (val_tt >= offsets.LUA_TSTRING) {
|
|
const val_gc = readU32(node + offsets.NODE_value_gcptr);
|
|
if (val_gc != 0) _ = markObject(val_gc);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_weak) weakTableListAdd(table);
|
|
return is_weak;
|
|
}
|
|
|
|
/// Traverse closure children. Matches Lua 5.1 traverseclosure (lgc.c:224-238).
|
|
fn traverseClosure(cl: u32) void {
|
|
const isC = readU8(cl + offsets.CLOSURE_isC);
|
|
const nups = readU8(cl + offsets.CLOSURE_nupvalues);
|
|
|
|
if (isC != 0) {
|
|
var i: u32 = 0;
|
|
while (i < nups) : (i += 1) {
|
|
const tv = cl + offsets.CLOSURE_c_upvals + i * offsets.TVALUE_size;
|
|
const tt = readU8(tv + offsets.TVALUE_tt);
|
|
if (tt >= offsets.LUA_TSTRING) {
|
|
const gc = readU32(tv + offsets.TVALUE_gcptr);
|
|
if (gc != 0) _ = markObject(gc);
|
|
}
|
|
}
|
|
} else {
|
|
// Lua closure: mark env table + proto
|
|
const m1 = readU32(cl + offsets.CLOSURE_lua_mark1);
|
|
if (m1 != 0) _ = markObject(m1);
|
|
const m2 = readU32(cl + offsets.CLOSURE_lua_mark2);
|
|
if (m2 != 0) _ = markObject(m2);
|
|
|
|
// Upvalues: 5.1 reallymarkobject for LUA_TUPVAL (lgc.c:83-88):
|
|
// white2gray(o), markvalue(g, uv->v), if closed: gray2black(o)
|
|
// We must make the upvalue itself non-white so sweep doesn't free it.
|
|
// Don't push to gray stack (upvals have no children to traverse).
|
|
var i: u32 = 0;
|
|
while (i < nups) : (i += 1) {
|
|
const upv = readU32(cl + offsets.CLOSURE_lua_upval_ptrs + i * 4);
|
|
if (upv == 0) continue;
|
|
|
|
// Make upvalue non-white (gray) so it survives sweep.
|
|
// Also serves as "processed" flag (native TEST AL,AL sees non-zero).
|
|
if (isWhite(upv)) {
|
|
white2gray(upv);
|
|
}
|
|
|
|
// Mark the upvalue's held value
|
|
const val_tt = readU8(upv + offsets.UPVAL_value_tt);
|
|
if (val_tt >= offsets.LUA_TSTRING) {
|
|
const val_gc = readU32(upv + offsets.UPVAL_value_gcptr);
|
|
if (val_gc != 0) _ = markObject(val_gc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Traverse a thread: globals table + stack slots up to max(top, all ci->top).
|
|
fn traverseThread(th: u32) void {
|
|
// Globals table TValue at thread+0x40..+0x48
|
|
const gt_tt = readU8(th + offsets.THREAD_gt_tt);
|
|
if (gt_tt >= offsets.LUA_TSTRING) {
|
|
const gt_gc = readU32(th + offsets.THREAD_gt_gcptr);
|
|
if (gt_gc != 0) _ = markObject(gt_gc);
|
|
}
|
|
|
|
// lim = max(L->top, all ci->top)
|
|
var lim: u32 = readU32(th + offsets.THREAD_top);
|
|
const base_ci = readU32(th + offsets.THREAD_base_ci);
|
|
const ci = readU32(th + offsets.THREAD_ci);
|
|
if (base_ci != 0 and ci != 0 and base_ci <= ci) {
|
|
var ca: u32 = base_ci;
|
|
while (ca <= ci) : (ca += 24) {
|
|
const ci_top = readU32(ca + 0x04);
|
|
if (ci_top > lim) lim = ci_top;
|
|
}
|
|
}
|
|
|
|
const stack_base = readU32(th + offsets.THREAD_stack);
|
|
const top = readU32(th + offsets.THREAD_top);
|
|
if (stack_base == 0 or top == 0 or stack_base >= top) return;
|
|
|
|
// Mark active stack slots: [stack, top)
|
|
var sp: u32 = stack_base;
|
|
while (sp < top) : (sp += offsets.TVALUE_size) {
|
|
const tt = readU8(sp + offsets.TVALUE_tt);
|
|
if (tt >= offsets.LUA_TSTRING) {
|
|
const gc = readU32(sp + offsets.TVALUE_gcptr);
|
|
if (gc != 0) _ = markObject(gc);
|
|
}
|
|
}
|
|
|
|
// Nil stale slots above top: [top, lim]
|
|
// Matches native: `for (; o <= lim; o++) setnilvalue(o);`
|
|
// Without this, stale GC pointers from dead call frames keep dead objects alive.
|
|
if (lim > top) {
|
|
sp = top;
|
|
while (sp <= lim) : (sp += offsets.TVALUE_size) {
|
|
writeU32(sp + offsets.TVALUE_tt, 0); // tt = LUA_TNIL
|
|
writeU32(sp + offsets.TVALUE_gcptr, 0); // value = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Traverse proto: source, constants, nested protos, upvalue names, locvars.
|
|
fn traverseProto(proto: u32) void {
|
|
const source = readU32(proto + offsets.PROTO_source);
|
|
if (source != 0) stringmark(source);
|
|
|
|
// Constants: mark strings, mark non-string collectables
|
|
const k_ptr = readU32(proto + offsets.PROTO_k);
|
|
const sizek = readU32(proto + offsets.PROTO_sizek);
|
|
if (k_ptr != 0) {
|
|
var i: u32 = 0;
|
|
while (i < sizek) : (i += 1) {
|
|
const tv = k_ptr + i * offsets.TVALUE_size;
|
|
const tt = readU8(tv + offsets.TVALUE_tt);
|
|
if (tt == offsets.LUA_TSTRING) {
|
|
const s = readU32(tv + offsets.TVALUE_gcptr);
|
|
if (s != 0) stringmark(s);
|
|
} else if (tt >= offsets.LUA_TSTRING) {
|
|
const gc = readU32(tv + offsets.TVALUE_gcptr);
|
|
if (gc != 0) _ = markObject(gc);
|
|
}
|
|
}
|
|
}
|
|
|
|
const upv_ptr = readU32(proto + offsets.PROTO_upvalues);
|
|
const sizeup = readU32(proto + offsets.PROTO_sizeupvalues);
|
|
if (upv_ptr != 0) {
|
|
var i: u32 = 0;
|
|
while (i < sizeup) : (i += 1) {
|
|
const s = readU32(upv_ptr + i * 4);
|
|
if (s != 0) stringmark(s);
|
|
}
|
|
}
|
|
|
|
const p_ptr = readU32(proto + offsets.PROTO_p);
|
|
const sizep = readU32(proto + offsets.PROTO_sizep);
|
|
if (p_ptr != 0) {
|
|
var i: u32 = 0;
|
|
while (i < sizep) : (i += 1) {
|
|
const sub = readU32(p_ptr + i * 4);
|
|
if (sub != 0) _ = markObject(sub);
|
|
}
|
|
}
|
|
|
|
const lv_ptr = readU32(proto + offsets.PROTO_locvars);
|
|
const sizelv = readU32(proto + offsets.PROTO_sizelocvars);
|
|
if (lv_ptr != 0) {
|
|
var i: u32 = 0;
|
|
while (i < sizelv) : (i += 1) {
|
|
const s = readU32(lv_ptr + i * offsets.LOCVAR_size);
|
|
if (s != 0) stringmark(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Root Mark
|
|
// =============================================================================
|
|
|
|
/// Mark root set. Matches Lua 5.1 markroot (lgc.c:501-512).
|
|
fn markroot(g: u32) void {
|
|
gray_count = 0;
|
|
grayagain_count = 0;
|
|
weak_table_count = 0;
|
|
|
|
// Bootstrap: on first cycle, existing objects from native GC have marked=0.
|
|
// Set them to currentwhite so our mark can find them.
|
|
if (needs_bootstrap) bootstrapWhite(g);
|
|
|
|
// defaultmeta
|
|
if (readU8(g + 0x40) >= offsets.LUA_TTABLE) {
|
|
const dm = readU32(g + offsets.GS_defaultmeta);
|
|
if (dm != 0) _ = markObject(dm);
|
|
}
|
|
|
|
// registry
|
|
if (readU8(g + 0x30) >= offsets.LUA_TTABLE) {
|
|
const reg = readU32(g + offsets.GS_registry);
|
|
if (reg != 0) _ = markObject(reg);
|
|
}
|
|
|
|
// mainthread
|
|
const mt = readU32(g + offsets.GS_mainthread);
|
|
_ = markObject(mt);
|
|
|
|
// Make global table be traversed before main stack (5.1 line 508)
|
|
const gt_tt = readU8(mt + offsets.THREAD_gt_tt);
|
|
if (gt_tt >= offsets.LUA_TSTRING) {
|
|
const gt_gc = readU32(mt + offsets.THREAD_gt_gcptr);
|
|
if (gt_gc != 0) _ = markObject(gt_gc);
|
|
}
|
|
|
|
// current L if different
|
|
if (saved_L != 0 and saved_L != mt) {
|
|
_ = markObject(saved_L);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Incremental Mark Step
|
|
// =============================================================================
|
|
|
|
/// Process up to CHUNK_SIZE objects from the gray stack.
|
|
/// Returns true when gray stack is drained (mark complete).
|
|
fn markStep() bool {
|
|
cur_mark_steps += 1;
|
|
var processed: u32 = 0;
|
|
|
|
while (gray_count > 0 and processed < CHUNK_SIZE) {
|
|
const obj = grayStackPop();
|
|
if (obj == 0) break;
|
|
propagatemark(obj);
|
|
processed += 1;
|
|
}
|
|
|
|
return gray_count == 0;
|
|
}
|
|
|
|
/// Drain the entire gray stack (used in atomic phase).
|
|
fn propagateall() void {
|
|
while (gray_count > 0) {
|
|
const obj = grayStackPop();
|
|
if (obj == 0) break;
|
|
propagatemark(obj);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Atomic Phase (between mark and sweep)
|
|
// =============================================================================
|
|
//
|
|
// Matches Lua 5.0 lgc.c mark() ordering:
|
|
// 1. cleartablevalues(wkv, wv)
|
|
// 2. luaC_separateudata(L)
|
|
// 3. marktmu
|
|
// 4. propagatemarks (drain gray stack from resurrected udata)
|
|
// 5. cleartablekeys(wk, wkv) + any newly weak tables
|
|
|
|
/// Atomic phase. Matches Lua 5.1 atomic() (lgc.c:525-553).
|
|
/// This runs to completion in a single call -- no yielding.
|
|
fn atomicPhase(L: u32, g: u32) void {
|
|
// (1) Propagate any remaining gray objects
|
|
propagateall();
|
|
|
|
// (2) Re-traverse weak tables (push to gray, propagate)
|
|
// In 5.1 this uses g->weak. We use our weak_table_list.
|
|
// Weak tables need re-traversal because they were kept gray.
|
|
var wi: u32 = 0;
|
|
while (wi < weak_table_count) : (wi += 1) {
|
|
const tbl = weak_table_list[wi];
|
|
grayStackPush(tbl);
|
|
}
|
|
propagateall();
|
|
|
|
// (3) Mark running thread + metatables (5.1 lines 536-537)
|
|
_ = markObject(saved_L);
|
|
// Mark defaultmeta again
|
|
if (readU8(g + 0x40) >= offsets.LUA_TTABLE) {
|
|
const dm = readU32(g + offsets.GS_defaultmeta);
|
|
if (dm != 0) _ = markObject(dm);
|
|
}
|
|
propagateall();
|
|
|
|
// (4) Re-traverse grayagain: barrier-back tables + threads
|
|
// 5.1 lines 539-542: g->gray = g->grayagain; propagateall
|
|
var gi: u32 = 0;
|
|
while (gi < grayagain_count) : (gi += 1) {
|
|
const obj = grayagain_list[gi];
|
|
// Push back to gray for re-traversal
|
|
grayStackPush(obj);
|
|
}
|
|
grayagain_count = 0;
|
|
propagateall();
|
|
|
|
// (5) Separate dead userdata with __gc into tmudata
|
|
native_separateudata(L);
|
|
|
|
// (6) Re-mark tmudata so their refs survive sweep
|
|
marktmuImpl(g);
|
|
propagateall();
|
|
|
|
// (7) Clear weak tables
|
|
atomicClearWeakTables();
|
|
|
|
// (8) Flip current white. New objects born after this point get the
|
|
// NEW currentwhite. Sweep will free objects with the OLD white.
|
|
currentwhite = otherwhite();
|
|
syncStringBirthMark(); // update string creation to use new white
|
|
|
|
weak_table_count = 0;
|
|
grayagain_count = 0;
|
|
}
|
|
|
|
/// Walk tmudata and re-mark each entry so references survive sweep.
|
|
fn marktmuImpl(g: u32) void {
|
|
var obj = readU32(g + offsets.GS_tmudata);
|
|
while (obj != 0) {
|
|
// Make white then re-mark (5.1 marktmu: makewhite + reallymarkobject)
|
|
makewhite(obj);
|
|
_ = markObject(obj); // white2gray + push to gray
|
|
obj = readU32(obj + offsets.OBJ_next);
|
|
}
|
|
}
|
|
|
|
/// Clear dead entries from weak tables collected during mark.
|
|
fn atomicClearWeakTables() void {
|
|
var i: u32 = 0;
|
|
while (i < weak_table_count) : (i += 1) {
|
|
const table = weak_table_list[i];
|
|
const marked = readU8(table + offsets.OBJ_marked);
|
|
if ((marked & offsets.VALUEWEAK) != 0) clearWeakValues(table);
|
|
if ((marked & offsets.KEYWEAK) != 0) clearWeakKeys(table);
|
|
}
|
|
}
|
|
|
|
/// Clear dead entries from weak-value tables. Matches native cleartablevalues.
|
|
/// For hash nodes, uses removekey semantics: nil value + set dead key to TNONE.
|
|
fn clearWeakValues(table: u32) void {
|
|
// Array part: just nil dead values
|
|
const array_ptr = readU32(table + offsets.TABLE_array);
|
|
const sizearray = readU32(table + offsets.TABLE_sizearray);
|
|
if (array_ptr != 0) {
|
|
var i: u32 = 0;
|
|
while (i < sizearray) : (i += 1) {
|
|
const tv = array_ptr + i * offsets.TVALUE_size;
|
|
const tt = readU8(tv + offsets.TVALUE_tt);
|
|
if (tt >= offsets.LUA_TSTRING) {
|
|
const gc = readU32(tv + offsets.TVALUE_gcptr);
|
|
if (gc != 0 and isWhite(gc)) {
|
|
writeU8(tv + offsets.TVALUE_tt, offsets.LUA_TNIL);
|
|
writeU32(tv + offsets.TVALUE_gcptr, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hash part: removekey -- nil value AND mark collectable keys as TNONE
|
|
const node_ptr = readU32(table + offsets.TABLE_node);
|
|
if (node_ptr == 0) return;
|
|
const lsizenode = readU8(table + offsets.TABLE_lsizenode);
|
|
const sizenode: u32 = @as(u32, 1) << @as(u5, @intCast(lsizenode));
|
|
|
|
var ni: u32 = 0;
|
|
while (ni < sizenode) : (ni += 1) {
|
|
const node = node_ptr + ni * offsets.NODE_size;
|
|
const val_tt = readU8(node + offsets.NODE_value_tt);
|
|
if (val_tt < offsets.LUA_TSTRING) continue;
|
|
const val_gc = readU32(node + offsets.NODE_value_gcptr);
|
|
if (val_gc == 0) continue;
|
|
if (isWhite(val_gc)) {
|
|
// removekey: nil value
|
|
writeU8(node + offsets.NODE_value_tt, offsets.LUA_TNIL);
|
|
writeU32(node + offsets.NODE_value_gcptr, 0);
|
|
// removekey: mark collectable key as dead
|
|
const key_tt = readU8(node + offsets.NODE_key_tt);
|
|
if (key_tt >= offsets.LUA_TSTRING) {
|
|
writeU8(node + offsets.NODE_key_tt, offsets.LUA_TNONE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clear dead entries from weak-key tables. Matches native cleartablekeys.
|
|
/// Uses removekey semantics: nil value + set dead collectable key to TNONE.
|
|
fn clearWeakKeys(table: u32) void {
|
|
const node_ptr = readU32(table + offsets.TABLE_node);
|
|
if (node_ptr == 0) return;
|
|
const lsizenode = readU8(table + offsets.TABLE_lsizenode);
|
|
const sizenode: u32 = @as(u32, 1) << @as(u5, @intCast(lsizenode));
|
|
|
|
var ni: u32 = 0;
|
|
while (ni < sizenode) : (ni += 1) {
|
|
const node = node_ptr + ni * offsets.NODE_size;
|
|
const key_tt = readU8(node + offsets.NODE_key_tt);
|
|
if (key_tt < offsets.LUA_TSTRING) continue;
|
|
const key_gc = readU32(node + offsets.NODE_key_gcptr);
|
|
if (key_gc == 0) continue;
|
|
if (isWhite(key_gc)) {
|
|
// removekey: nil value
|
|
writeU8(node + offsets.NODE_value_tt, offsets.LUA_TNIL);
|
|
writeU32(node + offsets.NODE_value_gcptr, 0);
|
|
// removekey: mark collectable key as dead
|
|
writeU8(node + offsets.NODE_key_tt, offsets.LUA_TNONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// From-Scratch Sweep: rootgc (incremental, cursor-based)
|
|
// =============================================================================
|
|
//
|
|
// Walks rootgc in place using a prev_next pointer. For each object:
|
|
// - If marked: clear mark bit, advance cursor (object survives)
|
|
// - If unmarked: unlink from list, call lua_gc_free_object, count freed
|
|
//
|
|
// No list splitting. sweep_prev_next points to the address of the "next"
|
|
// field that leads to the current object (initially &g->rootgc).
|
|
|
|
fn sweepRootgcStep(L: u32) bool {
|
|
cur_sweep_steps += 1;
|
|
var processed: u32 = 0;
|
|
|
|
while (processed < CHUNK_SIZE) {
|
|
const obj = readU32(sweep_prev_next);
|
|
if (obj == 0) return true; // end of list, sweep done
|
|
|
|
if (!isDead(obj)) {
|
|
// Alive: set to current white for next cycle.
|
|
// Preserves non-color flags (KEYWEAK, VALUEWEAK, FIXED).
|
|
makewhite(obj);
|
|
sweep_prev_next = obj + offsets.OBJ_next;
|
|
} else {
|
|
// Dead: unlink and free
|
|
const next = readU32(obj + offsets.OBJ_next);
|
|
writeU32(sweep_prev_next, next);
|
|
native_free_object(L, obj);
|
|
cur_dead_freed += 1;
|
|
}
|
|
processed += 1;
|
|
}
|
|
|
|
// Bump threshold so GC doesn't re-trigger immediately
|
|
bumpThresholdHeadroom(saved_g);
|
|
return false; // more to sweep
|
|
}
|
|
|
|
// =============================================================================
|
|
// From-Scratch Sweep: rootudata (atomic, typically small)
|
|
// =============================================================================
|
|
|
|
fn sweepRootudata(L: u32, g: u32) void {
|
|
var prev_next: u32 = g + offsets.GS_rootudata;
|
|
while (true) {
|
|
const obj = readU32(prev_next);
|
|
if (obj == 0) break;
|
|
if (!isDead(obj)) {
|
|
makewhite(obj);
|
|
prev_next = obj + offsets.OBJ_next;
|
|
} else {
|
|
const next = readU32(obj + offsets.OBJ_next);
|
|
writeU32(prev_next, next);
|
|
native_free_object(L, obj);
|
|
cur_dead_freed += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// From-Scratch Sweep: string hash table (incremental, chunked by bucket)
|
|
// =============================================================================
|
|
//
|
|
// Walks g->strt.hash[bucket] chains. For each bucket:
|
|
// walk chain, unlink unmarked strings, call lua_gc_free_object.
|
|
// Decrement strt.nuse for each freed string.
|
|
// Chunks by processing N buckets per call.
|
|
|
|
fn sweepStringsStep(L: u32, g: u32) bool {
|
|
cur_sweep_steps += 1;
|
|
const hash_array = readU32(g + offsets.GS_strt_hash);
|
|
const bucket_count = readU32(g + offsets.GS_strt_size);
|
|
|
|
if (hash_array == 0 or bucket_count == 0) return true;
|
|
|
|
var buckets_done: u32 = 0;
|
|
while (sweep_string_bucket < bucket_count and buckets_done < CHUNK_SIZE) {
|
|
var prev_next: u32 = hash_array + sweep_string_bucket * 4;
|
|
while (true) {
|
|
const obj = readU32(prev_next);
|
|
if (obj == 0) break;
|
|
if (!isDead(obj)) {
|
|
makewhite(obj);
|
|
prev_next = obj + offsets.OBJ_next;
|
|
} else {
|
|
const next = readU32(obj + offsets.OBJ_next);
|
|
writeU32(prev_next, next);
|
|
const nuse = readU32(g + offsets.GS_strt_nuse);
|
|
if (nuse > 0) writeU32(g + offsets.GS_strt_nuse, nuse - 1);
|
|
native_free_object(L, obj);
|
|
cur_strings_freed += 1;
|
|
}
|
|
}
|
|
sweep_string_bucket += 1;
|
|
buckets_done += 1;
|
|
}
|
|
|
|
if (sweep_string_bucket >= bucket_count) return true;
|
|
|
|
bumpThresholdHeadroom(g);
|
|
return false;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Threshold Management
|
|
// =============================================================================
|
|
|
|
/// After sweep: threshold = 2 * totalbytes (matches Lua 5.0 checkSizes).
|
|
/// The native formula is `2 * nblocks - deadmem` but deadmem (size of
|
|
/// finalized udata) is typically negligible, so 2x is close enough.
|
|
fn setFinalThreshold(g: u32) void {
|
|
const totalbytes = readU32(g + offsets.GS_totalbytes);
|
|
writeU32(g + offsets.GS_gcthreshold, totalbytes *| 2);
|
|
}
|
|
|
|
/// Between incremental steps: bump threshold just enough to not retrigger.
|
|
fn bumpThresholdHeadroom(g: u32) void {
|
|
const totalbytes = readU32(g + offsets.GS_totalbytes);
|
|
writeU32(g + offsets.GS_gcthreshold, totalbytes + offsets.BATCH_HEADROOM);
|
|
}
|
|
|
|
/// Shrink string table if load factor is low (nuse < size/4).
|
|
fn maybeShrinkStringTable(L: u32, g: u32) void {
|
|
const nuse = readU32(g + offsets.GS_strt_nuse);
|
|
const size = readU32(g + offsets.GS_strt_size);
|
|
// Minimum string table size is 32 (MINSTRTABSIZE*2 in Lua 5.0)
|
|
if (nuse < size / 4 and size > 32) {
|
|
native_string_hash_resize(L, size / 2);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// luaC_collectgarbage Detour -- Incremental State Machine
|
|
// =============================================================================
|
|
|
|
/// Diagnostic modes:
|
|
/// 0 = our GC (normal)
|
|
/// 1 = full native passthrough (allocator only)
|
|
/// 2 = native mark + our sweep (isolates sweep bugs)
|
|
/// 3 = our mark + native sweep (isolates mark bugs)
|
|
const DIAG_MODE: u32 = 0;
|
|
|
|
fn collectGarbageDetour(L: u32) callconv(hook.cc.fastcall) void {
|
|
if (DIAG_MODE == 1) return collect_hook.callOriginal(.{L});
|
|
|
|
if (in_gc) return;
|
|
if (readU32(L + offsets.L_active_check) == 0) return;
|
|
|
|
in_gc = true;
|
|
defer in_gc = false;
|
|
|
|
const g = getGlobalState(L);
|
|
|
|
saved_L = L;
|
|
saved_g = g;
|
|
|
|
// Mode 2: native mark + our sweep -- isolates sweep bugs
|
|
if (DIAG_MODE == 2) {
|
|
_ = native_mark(L); // full native mark + atomic (weak clear, separateudata, marktmu)
|
|
// Our sweep
|
|
sweep_prev_next = g + offsets.GS_rootgc;
|
|
var done = false;
|
|
while (!done) done = sweepRootgcStep(L);
|
|
sweepRootudata(L, g);
|
|
sweep_string_bucket = 0;
|
|
done = false;
|
|
while (!done) done = sweepStringsStep(L, g);
|
|
// (birth mark removed -- two-white handles this)
|
|
const mt = readU32(g + offsets.GS_mainthread);
|
|
makewhite(mt);
|
|
maybeShrinkStringTable(L, g);
|
|
setFinalThreshold(g);
|
|
native_callGCTM(L);
|
|
return;
|
|
}
|
|
|
|
// Mode 3: our mark + native sweep -- isolates mark bugs
|
|
if (DIAG_MODE == 3) {
|
|
// Our mark + atomic
|
|
gray_count = 0;
|
|
weak_table_count = 0;
|
|
markroot(g);
|
|
while (gray_count > 0) {
|
|
const obj = grayStackPop();
|
|
if (obj == 0) break;
|
|
propagatemark(obj);
|
|
}
|
|
atomicPhase(L, g);
|
|
// Native sweep + checkSizes + callGCTM
|
|
const native_sweep: *const fn (u32, u32) callconv(hook.cc.fastcall) void = @ptrFromInt(offsets.lua_gc_sweep_all_lists);
|
|
_ = native_sweep; // sweepstrings only
|
|
// Use the individual native sweep functions
|
|
const sweeplist_fn = *const fn (u32, u32, u32) callconv(hook.cc.fastcall) u32;
|
|
const native_sweeplist: sweeplist_fn = @ptrFromInt(offsets.lua_gc_remove_objects);
|
|
const native_sweepstrings: *const fn (u32, u32) callconv(hook.cc.fastcall) void = @ptrFromInt(offsets.lua_gc_sweep_all_lists);
|
|
_ = native_sweeplist(L, g + offsets.GS_rootudata, 0);
|
|
native_sweepstrings(L, 0);
|
|
_ = native_sweeplist(L, g + offsets.GS_rootgc, 0);
|
|
const mt = readU32(g + offsets.GS_mainthread);
|
|
makewhite(mt);
|
|
native_shrink_memory(L);
|
|
native_callGCTM(L);
|
|
return;
|
|
}
|
|
|
|
// Mode 4: our mark + our atomic + our sweep, but all-at-once (no state machine)
|
|
// Tests whether the state machine itself is the bug.
|
|
if (DIAG_MODE == 4) {
|
|
gray_count = 0;
|
|
weak_table_count = 0;
|
|
markroot(g);
|
|
while (gray_count > 0) {
|
|
const obj = grayStackPop();
|
|
if (obj == 0) break;
|
|
propagatemark(obj);
|
|
}
|
|
atomicPhase(L, g);
|
|
sweep_prev_next = g + offsets.GS_rootgc;
|
|
var done = false;
|
|
while (!done) done = sweepRootgcStep(L);
|
|
sweepRootudata(L, g);
|
|
sweep_string_bucket = 0;
|
|
done = false;
|
|
while (!done) done = sweepStringsStep(L, g);
|
|
// (birth mark removed -- two-white handles this)
|
|
const mt = readU32(g + offsets.GS_mainthread);
|
|
makewhite(mt);
|
|
if (saved_L != 0 and saved_L != mt) makewhite(saved_L);
|
|
maybeShrinkStringTable(L, g);
|
|
setFinalThreshold(g);
|
|
native_callGCTM(L);
|
|
return;
|
|
}
|
|
|
|
// Log entry state for first 20 cycles
|
|
if (stats.cycles_total < 20 and phase == .idle) {
|
|
const tb = readU32(g + offsets.GS_totalbytes);
|
|
const thr = readU32(g + offsets.GS_gcthreshold);
|
|
var buf: [128]u8 = undefined;
|
|
log.print(fmt(&buf, " trigger: totalbytes={d} threshold={d}\n", .{ tb, thr }));
|
|
}
|
|
|
|
switch (phase) {
|
|
.idle => {
|
|
// Start a new GC cycle
|
|
gray_count = 0;
|
|
weak_table_count = 0;
|
|
grayagain_count = 0;
|
|
cur_mark_steps = 0;
|
|
cur_sweep_steps = 0;
|
|
cur_dead_freed = 0;
|
|
cur_strings_freed = 0;
|
|
dbg_table_count = 0;
|
|
dbg_closure_count = 0;
|
|
dbg_thread_count = 0;
|
|
dbg_proto_count = 0;
|
|
dbg_udata_count = 0;
|
|
dbg_other_count = 0;
|
|
|
|
// Push roots and start marking.
|
|
// Birth mark stays OFF during mark and rootgc sweep. New rootgc
|
|
// objects prepend to the head (behind our forward cursor) and are
|
|
// safe. Birth mark is only needed during string sweep where new
|
|
// strings can land in unswept hash buckets ahead of the cursor.
|
|
markroot(g);
|
|
phase = .marking;
|
|
|
|
// Do one mark step immediately
|
|
if (markStep()) {
|
|
phase = .atomic;
|
|
runAtomicAndStartSweep(L, g);
|
|
} else {
|
|
bumpThresholdHeadroom(g);
|
|
}
|
|
},
|
|
|
|
.marking => {
|
|
if (markStep()) {
|
|
phase = .atomic;
|
|
runAtomicAndStartSweep(L, g);
|
|
} else {
|
|
bumpThresholdHeadroom(g);
|
|
}
|
|
},
|
|
|
|
.atomic => {
|
|
// Should not be called in this phase (atomic runs to completion)
|
|
// but handle gracefully
|
|
runAtomicAndStartSweep(L, g);
|
|
},
|
|
|
|
.sweep_strings => {
|
|
if (sweepStringsStep(L, g)) {
|
|
sweep_prev_next = g + offsets.GS_rootgc;
|
|
phase = .sweeping;
|
|
if (sweepRootgcStep(L)) {
|
|
phase = .finalize;
|
|
finalizeCycle(L, g);
|
|
}
|
|
}
|
|
},
|
|
|
|
.sweeping => {
|
|
if (sweepRootgcStep(L)) {
|
|
phase = .finalize;
|
|
finalizeCycle(L, g);
|
|
}
|
|
},
|
|
|
|
.finalize => {
|
|
// Should not happen (finalize runs to completion), handle gracefully
|
|
finalizeCycle(L, g);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Run atomic phase then transition to incremental sweep.
|
|
/// Sweep order matches native: rootudata (atomic) -> strings -> rootgc.
|
|
///
|
|
/// Birth mark is ON only during string sweep: new strings can hash into
|
|
/// unswept buckets ahead of the cursor and would be incorrectly freed.
|
|
/// Start sweep after atomic phase.
|
|
/// Two-white eliminates birth mark: new objects born with currentwhite
|
|
/// (already flipped in atomic), which is NOT otherwhite, so they survive.
|
|
fn runAtomicAndStartSweep(L: u32, g: u32) void {
|
|
atomicPhase(L, g);
|
|
|
|
// Sweep rootudata atomically (small list, fast)
|
|
sweepRootudata(L, g);
|
|
|
|
// Start chunked string sweep
|
|
sweep_string_bucket = 0;
|
|
phase = .sweep_strings;
|
|
|
|
if (sweepStringsStep(L, g)) {
|
|
sweep_prev_next = g + offsets.GS_rootgc;
|
|
phase = .sweeping;
|
|
if (sweepRootgcStep(L)) {
|
|
phase = .finalize;
|
|
finalizeCycle(L, g);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Final cleanup: shrink string table, set threshold, run finalizers.
|
|
fn finalizeCycle(L: u32, g: u32) void {
|
|
// Mainthread is not in rootgc (WoW-specific). Make it white for next cycle.
|
|
const mt = readU32(g + offsets.GS_mainthread);
|
|
makewhite(mt);
|
|
if (saved_L != 0 and saved_L != mt) {
|
|
makewhite(saved_L);
|
|
}
|
|
|
|
maybeShrinkStringTable(L, g);
|
|
setFinalThreshold(g);
|
|
|
|
// Run __gc finalizers
|
|
native_callGCTM(L);
|
|
|
|
// Commit stats
|
|
stats.cycles_total += 1;
|
|
stats.mark_steps_last = cur_mark_steps;
|
|
stats.sweep_steps_last = cur_sweep_steps;
|
|
stats.dead_freed_last = cur_dead_freed;
|
|
stats.strings_freed_last = cur_strings_freed;
|
|
|
|
if (stats.cycles_total <= 20 or stats.cycles_total % 100 == 0) {
|
|
const tb = readU32(g + offsets.GS_totalbytes);
|
|
const thr = readU32(g + offsets.GS_gcthreshold);
|
|
var buf: [300]u8 = undefined;
|
|
log.print(fmt(&buf, "cycle {d}: mark={d} sweep={d} | freed={d}+{d}str | totalbytes={d} threshold={d} | tbl={d} cl={d} th={d} proto={d} ud={d} other={d}\n", .{
|
|
stats.cycles_total, stats.mark_steps_last, stats.sweep_steps_last,
|
|
stats.dead_freed_last, stats.strings_freed_last, tb,
|
|
thr, dbg_table_count, dbg_closure_count,
|
|
dbg_thread_count, dbg_proto_count, dbg_udata_count,
|
|
dbg_other_count,
|
|
}));
|
|
}
|
|
|
|
phase = .idle;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Write Barriers (tri-color invariant during incremental mark)
|
|
// =============================================================================
|
|
|
|
const TableSetFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) u32;
|
|
|
|
var table_set_hook: hook.Detour(TableSetFn) = .{};
|
|
var table_set_int_hook: hook.Detour(TableSetFn) = .{};
|
|
|
|
fn tableSetBarrier(L: u32, table: u32, key: u32) callconv(hook.cc.fastcall) u32 {
|
|
// Barrier back (5.1 luaC_barrierback): if table is BLACK during propagate,
|
|
// make it gray again and add to grayagain list.
|
|
if (phase == .marking and isBlack(table)) {
|
|
black2gray(table);
|
|
grayagainAdd(table);
|
|
}
|
|
return table_set_hook.callOriginal(.{ L, table, key });
|
|
}
|
|
|
|
fn tableSetIntBarrier(L: u32, table: u32, int_key: u32) callconv(hook.cc.fastcall) u32 {
|
|
if (phase == .marking and isBlack(table)) {
|
|
black2gray(table);
|
|
grayagainAdd(table);
|
|
}
|
|
return table_set_int_hook.callOriginal(.{ L, table, int_key });
|
|
}
|
|
|
|
// =============================================================================
|
|
// lua_setgcthreshold hook
|
|
// =============================================================================
|
|
//
|
|
// WoW (and the Lua collectgarbage() function) calls lua_setgcthreshold to
|
|
// force GC by setting a low threshold. We intercept this: if the caller wants
|
|
// to force GC (low threshold), we run our GC cycle directly. Otherwise we
|
|
// let the threshold through but cap it to at least our 2x floor.
|
|
|
|
const SetGcThresholdFn = fn (u32, u32) callconv(hook.cc.fastcall) void;
|
|
var setgcthreshold_hook: hook.Detour(SetGcThresholdFn) = .{};
|
|
|
|
fn setgcthresholdDetour(L: u32, newthreshold: u32) callconv(hook.cc.fastcall) void {
|
|
// The caller wants to set gcthreshold = newthreshold << 10.
|
|
// If it's a low value (forcing GC), just run our GC cycle.
|
|
// The original also calls luaC_checkGC after setting, which would
|
|
// trigger our collectGarbageDetour anyway. Skip the threshold write
|
|
// entirely -- our GC owns the threshold.
|
|
_ = newthreshold;
|
|
if (readU32(L + offsets.L_active_check) == 0) return;
|
|
const g = getGlobalState(L);
|
|
const tb = readU32(g + offsets.GS_totalbytes);
|
|
const thr = readU32(g + offsets.GS_gcthreshold);
|
|
// If threshold is already above totalbytes, no need to force GC
|
|
if (thr > tb) return;
|
|
// Otherwise trigger a GC by calling our detour path
|
|
collectGarbageDetour(L);
|
|
}
|
|
|
|
// =============================================================================
|
|
// lua_close hook
|
|
// =============================================================================
|
|
//
|
|
// lua_close (0x6F6EF0) calls luaC_sweep(L,1) directly, bypassing our
|
|
// collectgarbage detour. If we're mid-cycle, our gray stack and sweep
|
|
// cursors hold pointers to objects that luaC_sweep is about to free.
|
|
// Reset our state to idle before the native close proceeds.
|
|
|
|
const LuaCloseFn = fn (u32) callconv(hook.cc.fastcall) void;
|
|
var lua_close_hook: hook.Detour(LuaCloseFn) = .{};
|
|
|
|
fn luaCloseDetour(L: u32) callconv(hook.cc.fastcall) void {
|
|
// Abandon any in-progress cycle
|
|
phase = .idle;
|
|
gray_count = 0;
|
|
grayagain_count = 0;
|
|
weak_table_count = 0;
|
|
in_gc = false;
|
|
currentwhite = 0x01;
|
|
needs_bootstrap = true;
|
|
syncStringBirthMark();
|
|
|
|
lua_close_hook.callOriginal(.{L});
|
|
}
|
|
|
|
// =============================================================================
|
|
// luaC_link hook (two-white birth system)
|
|
// =============================================================================
|
|
//
|
|
// New objects must be born with currentwhite so they're alive in the current
|
|
// cycle. The native luaC_link sets marked=0 which in our two-white system
|
|
// means "gray" (invisible to mark). We hook it to set marked=currentwhite.
|
|
|
|
const LuaCLinkFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) void;
|
|
var luac_link_hook: hook.Detour(LuaCLinkFn) = .{};
|
|
|
|
fn luaCLinkDetour(L: u32, obj: u32, tt: u32) callconv(hook.cc.fastcall) void {
|
|
const g = getGlobalState(L);
|
|
writeU32(obj + offsets.OBJ_next, readU32(g + offsets.GS_rootgc));
|
|
writeU32(g + offsets.GS_rootgc, obj);
|
|
writeU8(obj + offsets.OBJ_marked, currentwhite);
|
|
writeU8(obj + offsets.OBJ_tt, @intCast(tt));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Forward barriers (luaC_barrierf equivalent)
|
|
// =============================================================================
|
|
//
|
|
// Lua 5.1 has luaC_barrierf at every write site that creates a cross-object
|
|
// reference (besides table writes which use barrierback). WoW's Lua 5.0 has
|
|
// no barriers at all. We hook each function that needs one.
|
|
//
|
|
// The barrier: if we're in marking phase, and the written value is a WHITE
|
|
// collectable GC object, mark it immediately so it survives sweep.
|
|
// This prevents BLACK→WHITE edges from going unnoticed.
|
|
|
|
/// Forward barrier: if a white collectable was written somewhere during
|
|
/// marking, mark it now. Reads a TValue at the given address.
|
|
fn barrierForward(tv_addr: u32) void {
|
|
if (phase != .marking) return;
|
|
const tt = readU8(tv_addr + offsets.TVALUE_tt);
|
|
if (tt < offsets.LUA_TSTRING) return; // not collectable
|
|
const gc = readU32(tv_addr + offsets.TVALUE_gcptr);
|
|
if (gc == 0) return;
|
|
if (isWhite(gc)) _ = markObject(gc);
|
|
}
|
|
|
|
// --- lua_setmetatable (0x6F4020) ---
|
|
// __fastcall(ECX=L, EDX=objindex). Sets metatable on stack object.
|
|
// 5.1 barriers: luaC_objbarriert for tables, luaC_objbarrier for userdata.
|
|
const SetMetaFn = fn (u32, u32) callconv(hook.cc.fastcall) u32;
|
|
var setmeta_hook: hook.Detour(SetMetaFn) = .{};
|
|
fn setmetaDetour(L: u32, objindex: u32) callconv(hook.cc.fastcall) u32 {
|
|
const result = setmeta_hook.callOriginal(.{ L, objindex });
|
|
// After the native sets the metatable, barrier the new metatable.
|
|
// The metatable was just pushed on the stack (L->top - 1) before the call,
|
|
// then popped by the native. We can't easily get the metatable pointer
|
|
// after the call. Instead, just re-mark the object's metatable field.
|
|
// The object is at the given stack index. Read it, get its metatable.
|
|
if (phase == .marking) {
|
|
// Resolve stack index to object pointer
|
|
const obj_tv = resolveStackIndex(L, objindex);
|
|
if (obj_tv != 0) {
|
|
const obj_tt = readU8(obj_tv + offsets.TVALUE_tt);
|
|
if (obj_tt == offsets.LUA_TTABLE or obj_tt == offsets.LUA_TUSERDATA) {
|
|
const obj_gc = readU32(obj_tv + offsets.TVALUE_gcptr);
|
|
if (obj_gc != 0) {
|
|
const mt = readU32(obj_gc + offsets.TABLE_metatable);
|
|
if (mt != 0 and isWhite(mt)) _ = markObject(mt);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// --- lua_setupvalue (0x6F47B0) ---
|
|
// __fastcall(ECX=L, EDX=funcindex, stack=n). Sets upvalue by index.
|
|
const SetUpvalFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) u32;
|
|
var setupval_hook: hook.Detour(SetUpvalFn) = .{};
|
|
fn setupvalDetour(L: u32, funcindex: u32, n: u32) callconv(hook.cc.fastcall) u32 {
|
|
const result = setupval_hook.callOriginal(.{ L, funcindex, n });
|
|
// After the native sets the upvalue, the value was popped from stack.
|
|
// The upvalue now holds the new value. We can't easily get which upvalue
|
|
// was written without re-deriving it. But the native already did the write.
|
|
// For safety: if we're marking, re-traverse the closure.
|
|
if (phase == .marking and result != 0) {
|
|
const obj_tv = resolveStackIndex(L, funcindex);
|
|
if (obj_tv != 0 and readU8(obj_tv + offsets.TVALUE_tt) == offsets.LUA_TFUNCTION) {
|
|
const cl = readU32(obj_tv + offsets.TVALUE_gcptr);
|
|
if (cl != 0 and isBlack(cl)) {
|
|
black2gray(cl);
|
|
grayagainAdd(cl);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// --- lua_pushcclosure (0x6F3920) ---
|
|
// __fastcall(ECX=L, EDX=fn_ptr, stack=n_upvals). Creates C closure with upvalues.
|
|
const PushCClosureFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) void;
|
|
var pushcclosure_hook: hook.Detour(PushCClosureFn) = .{};
|
|
fn pushcclosureDetour(L: u32, fn_ptr: u32, n: u32) callconv(hook.cc.fastcall) void {
|
|
pushcclosure_hook.callOriginal(.{ L, fn_ptr, n });
|
|
// The new closure is at L->top - 1. It was just created (WHITE).
|
|
// Its upvalues were set from the stack. The closure is WHITE, so
|
|
// no BLACK→WHITE edge (barrier only matters if container is BLACK).
|
|
// New closure is currentwhite → no barrier needed.
|
|
}
|
|
|
|
// --- lua_setfenv (0x6F40D0) ---
|
|
// __fastcall(ECX=L, EDX=idx). Sets environment table on stack object.
|
|
const SetFenvFn = fn (u32, u32) callconv(hook.cc.fastcall) u32;
|
|
var setfenv_hook: hook.Detour(SetFenvFn) = .{};
|
|
fn setfenvDetour(L: u32, idx: u32) callconv(hook.cc.fastcall) u32 {
|
|
const result = setfenv_hook.callOriginal(.{ L, idx });
|
|
if (phase == .marking and result != 0) {
|
|
const obj_tv = resolveStackIndex(L, idx);
|
|
if (obj_tv != 0) {
|
|
const obj_gc = readU32(obj_tv + offsets.TVALUE_gcptr);
|
|
if (obj_gc != 0 and isBlack(obj_gc)) {
|
|
black2gray(obj_gc);
|
|
grayagainAdd(obj_gc);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Resolve a Lua stack index to the TValue address.
|
|
/// Positive = from base, negative = from top, pseudo-indices = special.
|
|
fn resolveStackIndex(L: u32, idx: u32) u32 {
|
|
const idx_i: i32 = @bitCast(idx);
|
|
if (idx_i > 0) {
|
|
// Positive: base + (idx-1) * 16
|
|
const base = readU32(L + 0x0C); // L->base (ci->base typically at L+0x0C)
|
|
if (base == 0) return 0;
|
|
return base + @as(u32, @intCast(idx_i - 1)) * offsets.TVALUE_size;
|
|
} else if (idx_i < -10000) {
|
|
// Pseudo-index (globals, registry, upvalues) -- skip
|
|
return 0;
|
|
} else {
|
|
// Negative: top + idx * 16
|
|
const top = readU32(L + offsets.THREAD_top);
|
|
if (top == 0) return 0;
|
|
return @as(u32, @intCast(@as(i32, @intCast(top)) + idx_i * @as(i32, offsets.TVALUE_size)));
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// OP_SETUPVAL VM patch (forward barrier)
|
|
// =============================================================================
|
|
//
|
|
// At 0x6F8A97, right after the 16-byte TValue copy in OP_SETUPVAL handler,
|
|
// EDX holds uv->v (the destination pointer -- the TValue just written).
|
|
// We patch 5 bytes at 0x6F8A97 with CALL to our barrier trampoline.
|
|
// The trampoline checks if the written value is WHITE and marks it.
|
|
//
|
|
// Original bytes at 0x6F8A97: 8B 7F 04 85 FF (MOV EDI,[EDI+4]; TEST EDI,EDI)
|
|
// These are debug hook code, not critical to SETUPVAL functionality.
|
|
// We overwrite with: E8 XX XX XX XX (CALL rel32 to our function)
|
|
// Our function does the barrier check then executes the overwritten instructions.
|
|
|
|
var setupval_patch_installed: bool = false;
|
|
var setupval_original_bytes: [5]u8 = undefined;
|
|
|
|
fn installSetupvalPatch() void {
|
|
// Read original bytes
|
|
const src: [*]const u8 = @ptrFromInt(0x6F8A97);
|
|
@memcpy(&setupval_original_bytes, src[0..5]);
|
|
|
|
// Write CALL rel32 to our barrier
|
|
const target = @intFromPtr(&setupvalBarrierTrampoline);
|
|
const rel: i32 = @intCast(@as(i64, target) - (0x6F8A97 + 5));
|
|
var patch: [5]u8 = undefined;
|
|
patch[0] = 0xE8; // CALL rel32
|
|
patch[1..5].* = @bitCast(rel);
|
|
hook.writeProtected(0x6F8A97, &patch);
|
|
setupval_patch_installed = true;
|
|
}
|
|
|
|
fn removeSetupvalPatch() void {
|
|
if (setupval_patch_installed) {
|
|
hook.writeProtected(0x6F8A97, &setupval_original_bytes);
|
|
setupval_patch_installed = false;
|
|
}
|
|
}
|
|
|
|
/// Called from the VM after OP_SETUPVAL's TValue copy.
|
|
/// EDX = uv->v (the TValue that was just written to).
|
|
/// Must preserve all registers and flags, execute the overwritten instructions,
|
|
/// then return.
|
|
fn setupvalBarrierTrampoline() callconv(.naked) void {
|
|
// Called from VM after OP_SETUPVAL TValue copy. EDX = uv->v.
|
|
// Check if written value is a white collectable during mark, mark it if so.
|
|
// Must preserve all registers and execute overwritten instructions before ret.
|
|
asm volatile (
|
|
\\ pushf
|
|
\\ push %%eax
|
|
\\ push %%ecx
|
|
// Check phase == .marking (1)
|
|
\\ movl %[phase_ptr], %%eax
|
|
\\ cmpb $1, (%%eax)
|
|
\\ jne 1f
|
|
// EDX = uv->v. Check tt >= LUA_TSTRING (4)
|
|
\\ movzbl (%%edx), %%eax
|
|
\\ cmpl $4, %%eax
|
|
\\ jb 1f
|
|
// Read gcptr at [EDX+8]
|
|
\\ movl 8(%%edx), %%eax
|
|
\\ testl %%eax, %%eax
|
|
\\ jz 1f
|
|
// Check if white (marked & 0x03)
|
|
\\ testb $0x03, 5(%%eax)
|
|
\\ jz 1f
|
|
// White collectable during mark -> call markObject via indirect
|
|
\\ push %%edx
|
|
\\ push %%eax
|
|
\\ movl %[mark_fn], %%ecx
|
|
\\ call *%%ecx
|
|
\\ addl $4, %%esp
|
|
\\ pop %%edx
|
|
\\1:
|
|
\\ pop %%ecx
|
|
\\ pop %%eax
|
|
\\ popf
|
|
// Execute overwritten: MOV EDI,[EDI+4]; TEST EDI,EDI
|
|
\\ movl 4(%%edi), %%edi
|
|
\\ testl %%edi, %%edi
|
|
\\ ret
|
|
:
|
|
: [phase_ptr] "i" (&phase),
|
|
[mark_fn] "i" (&markObjectCdecl),
|
|
);
|
|
}
|
|
|
|
fn markObjectCdecl(obj: u32) callconv(.c) void {
|
|
_ = markObject(obj);
|
|
}
|
|
|
|
// =============================================================================
|
|
// ZGCStats Lua C function
|
|
// =============================================================================
|
|
|
|
pub fn luaZGCStats(L: lua.State) callconv(hook.cc.fastcall) i32 {
|
|
lua.pushnumber(L, @floatFromInt(stats.cycles_total));
|
|
lua.pushnumber(L, @floatFromInt(stats.mark_steps_last));
|
|
lua.pushnumber(L, @floatFromInt(stats.sweep_steps_last));
|
|
lua.pushnumber(L, @floatFromInt(stats.gray_peak));
|
|
lua.pushnumber(L, @floatFromInt(stats.dead_freed_last));
|
|
lua.pushnumber(L, @floatFromInt(stats.strings_freed_last));
|
|
lua.pushnumber(L, @floatFromInt(@intFromEnum(phase)));
|
|
return 7;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Installation
|
|
// =============================================================================
|
|
|
|
const CollectFn = fn (u32) callconv(hook.cc.fastcall) void;
|
|
var collect_hook: hook.Detour(CollectFn) = .{};
|
|
|
|
pub fn isActive() bool {
|
|
return g_is_hook_owner;
|
|
}
|
|
|
|
pub fn installHooks() void {
|
|
const result = mod_mutex.acquire(module_name);
|
|
g_mutex = result.handle;
|
|
g_is_hook_owner = result.is_owner;
|
|
if (!g_is_hook_owner) return;
|
|
|
|
log = logging.Logger.open(module_name, .both);
|
|
var installed: u32 = 0;
|
|
|
|
// Install slab allocator
|
|
installed += luaalloc.install();
|
|
|
|
if (collect_hook.attach(offsets.luaC_collectgarbage, &collectGarbageDetour) == .ok) {
|
|
installed += 1;
|
|
log.print("hooked luaC_collectgarbage\n");
|
|
}
|
|
if (table_set_hook.attach(offsets.lua_table_set_value, &tableSetBarrier) == .ok) {
|
|
installed += 1;
|
|
log.print("hooked lua_table_set_value\n");
|
|
}
|
|
if (table_set_int_hook.attach(offsets.lua_table_set_int_key, &tableSetIntBarrier) == .ok) {
|
|
installed += 1;
|
|
log.print("hooked lua_table_set_int_key\n");
|
|
}
|
|
if (setgcthreshold_hook.attach(offsets.lua_setgcthreshold, &setgcthresholdDetour) == .ok) {
|
|
installed += 1;
|
|
log.print("hooked lua_setgcthreshold\n");
|
|
}
|
|
if (lua_close_hook.attach(offsets.lua_close, &luaCloseDetour) == .ok) {
|
|
installed += 1;
|
|
log.print("hooked lua_close\n");
|
|
}
|
|
if (luac_link_hook.attach(offsets.luaC_link, &luaCLinkDetour) == .ok) {
|
|
installed += 1;
|
|
log.print("hooked luaC_link\n");
|
|
}
|
|
// Forward barriers (5.1 luaC_barrierf equivalents)
|
|
if (setmeta_hook.attach(offsets.lua_setmetatable, &setmetaDetour) == .ok) {
|
|
installed += 1;
|
|
}
|
|
if (setupval_hook.attach(offsets.lua_setupvalue, &setupvalDetour) == .ok) {
|
|
installed += 1;
|
|
}
|
|
if (setfenv_hook.attach(offsets.lua_setfenv, &setfenvDetour) == .ok) {
|
|
installed += 1;
|
|
}
|
|
// Note: lua_pushcclosure doesn't need a barrier (new closures are WHITE)
|
|
installSetupvalPatch();
|
|
log.print("installed forward barriers\n");
|
|
|
|
syncStringBirthMark();
|
|
|
|
var buf: [64]u8 = undefined;
|
|
log.print(fmt(&buf, "installed {d} hooks\n", .{installed}));
|
|
}
|
|
|
|
pub fn removeHooks() void {
|
|
if (!g_is_hook_owner) return;
|
|
|
|
// If mid-cycle, restore birth mark
|
|
// Reset string birth mark to 0 (native default)
|
|
var patch = [1]u8{0x00};
|
|
hook.writeProtected(offsets.birth_mark_string, &patch);
|
|
|
|
collect_hook.detach();
|
|
table_set_hook.detach();
|
|
table_set_int_hook.detach();
|
|
setgcthreshold_hook.detach();
|
|
lua_close_hook.detach();
|
|
luac_link_hook.detach();
|
|
setmeta_hook.detach();
|
|
setupval_hook.detach();
|
|
setfenv_hook.detach();
|
|
removeSetupvalPatch();
|
|
luaalloc.remove();
|
|
|
|
phase = .idle;
|
|
in_gc = false;
|
|
gray_count = 0;
|
|
|
|
log.close();
|
|
mod_mutex.release(&g_mutex);
|
|
}
|