Type Mirrors¶
src/mp_mirrors.zig (~1,200 lines) collects the ~40 extern struct definitions that mirror MicroPython's C structs field-for-field, with @sizeOf and @offsetOf asserts.
This lets Zig read and write the same memory that vendor MicroPython C did, by name.
The problem mirrors solve¶
MicroPython's runtime is full of structs like this (vendor C):
typedef struct _mp_obj_list_t {
mp_obj_base_t base;
size_t alloc;
size_t len;
mp_obj_t *items;
} mp_obj_list_t;
When the strangler ports a function that operates on a mp_obj_list_t, the Zig version needs to access the same fields, at the same offsets, with the same types. If Zig writes to offset 16 thinking it's len but C reads from offset 24 thinking it's len, you get data corruption.
The conventional solution is @cImport, which runs the C header through translate-c and exposes the struct as a Zig type. That works for simple cases but breaks down for:
- Macros (
MP_OBJ_NEW_SMALL_INT,MP_OBJ_FROM_PTR): translate-c can't always reproduce these - Anonymous types:
translate-cgeneratesstruct_unnamed_Nnames that don't@ptrCastcleanly between modules - Build-time configuration variation:
translate-cruns in a separate pass; macros set after the@cIncludearen't visible
The mirror approach sidesteps all of these by declaring the type manually in Zig with extern struct (which guarantees C layout) and verifying the layout matches with @sizeOf / @offsetOf asserts.
Anatomy of a mirror¶
/// Mirror of `mp_obj_list_t` from vendor `objlist.h:13-19`.
///
/// Fields must match exactly. Verified by `assertLayout()`.
pub const MpObjList = extern struct {
base: MpObjBase, // 16 bytes on 64-bit (type ptr + padding)
alloc: usize, // 8
len: usize, // 8
items: ?[*]Obj, // 8
};
// In assertLayout():
std.debug.assert(@sizeOf(MpObjList) == npy_mirror_sizeof_mp_obj_list_t);
std.debug.assert(@offsetOf(MpObjList, "items") == npy_mirror_offsetof_mp_obj_list_t_items);
The npy_mirror_* consts come from a tiny C file (src/mp_layout_check.zig defines them as exports computed from the actual C structs). If Zig's MpObjList drifts from vendor's mp_obj_list_t, the assert fires at runtime startup and the binary panics with a layout error.
Today the asserts are tautological (Zig MpObjList checked against Zig npy_mirror_sizeof_*, both Zig). The original intent was to use @cImport to compare against the actual C type; documented as a follow-up. The mirror still catches "I broke MpObjList in Zig" but not "vendor changed mp_obj_list_t upstream." Acceptable because vendor is pinned via the symlink.
What's in mp_mirrors.zig¶
Roughly 50 mirror types covering:
| Category | Examples |
|---|---|
| Object base / type system | MpObjBase, MpObjType, MpObjFullType, MpObjTypeWithSlots |
| Built-in object types | MpObjList, MpObjDict, MpObjTuple, MpObjStr, MpObjModule |
| Function types | MpObjFunBc, MpObjFunBuiltinFixed, MpObjFunBuiltinVar, MpObjBoundMeth, MpObjClosure |
| Exception types | MpObjException, MpExcStack |
| State singletons | MpStateCtx, MpStateThread, MpStateVm, MpStateMem |
| Compilation types | MpRawCode, MpCompiledModule, MpModuleContext, MpReader, MpPrint |
| Map / qstr internals | MpMap, MpMapElem, MpModuleContextConstants |
| Iterator types | MpObjGenInstance, MpObjCell, MpObjGetitemIter |
Each mirror has a comment pointing at the vendor C declaration (e.g., // Mirror of mp_obj_list_t (objlist.h:13-19)). When the mirror is wrong, that comment is the first thing to recheck against upstream.
Helper functions in mirrors¶
Beyond types, mp_mirrors.zig exports inline helpers for the common operations:
// Object construction
pub inline fn objFromQstr(q: Qstr) Obj { ... }
pub inline fn objFromPtr(p: *const anyopaque) Obj { ... }
pub inline fn newSmallInt(v: isize) Obj { ... }
pub inline fn newBool(b: bool) Obj { ... }
// Object inspection
pub inline fn objBaseType(obj: Obj) *const anyopaque { ... }
pub inline fn isExactType(obj: Obj, ty: *const anyopaque) bool { ... }
pub inline fn smallIntValue(obj: Obj) isize { ... }
pub inline fn isSmallInt(obj: Obj) bool { ... }
// Type slot access
pub const MpObjType = struct {
pub fn fromPtr(p: *const anyopaque) MpObjType { ... }
pub fn hasSlot(self: MpObjType, slot: TypeSlot) bool { ... }
pub fn getSlot(self: MpObjType, slot: TypeSlot) ?*const anyopaque { ... }
pub fn flagX(self: MpObjType) bool { ... }
};
These wrap the bit-manipulation needed to encode/decode mp_obj_t values per REPR_A's tagged-pointer scheme. Using them keeps the rest of the code free of magic-number bit twiddles.
Notable mirrors¶
MpStateCtx: the global state singleton¶
MicroPython's mp_state_ctx is the top-level state struct: a volatile struct containing:
MpStateThread thread: the current thread's NLR stack, current locals/globals, recursion depthMpStateVm vm: the qstr pool, the dict_main, mp_loaded_modules_dict, sys.path, etc.MpStateMem mem: GC state, allocator pointers
Mirrored exactly in Zig as MpStateCtx with offset asserts that pin every byte. Accessing the singleton:
The mirror replaced what was originally a C shim file (zigonly_state.zig) that exposed accessors. Once the mirror matched byte-for-byte, the accessors became unnecessary.
MpObjType: the type system¶
mp_obj_type_t is the heart of MicroPython's dispatch. It contains:
- Type metadata (
name,flags,protocol) - Slot dispatch pointers (
make_new,print,call,unary_op,binary_op,attr,subscr,iter,buffer,protocol,parent,locals_dict)
The slot array is compressed in vendor C: the type stores indices into a global slot table rather than direct pointers, saving 4-8 bytes per slot. The MpObjType mirror replicates this via slot_index_*: u8 fields and a slots: [N]?*const anyopaque array.
Helpers like tm.getSlot(.binary_op) decode the compression and return the actual function pointer (or null if the slot isn't set).
MpRawCode: the bytecode container¶
A mp_raw_code_t represents a compiled function. Its fields:
proto_fun_indicator: [2]u8: disambiguates raw_code (bytecode) from native code via two leading byteskind: u8: one of BYTECODE, NATIVE_PY, NATIVE_VIPER, NATIVE_ASMis_generator: bool: does this function yield?fun_data: ?*const anyopaque: pointer to the bytecode or native codechildren: [*c]?*MpRawCode: array of nested raw_codes (one per nested function/class)
This mirror was added in NX-1 (commit 2f7c1c3) along with MpCompiledModule, MpReader, MpPrint, MpBytecodePrelude, and the constants BYTES_FOR_INT, QSTR_LAST_STATIC, MP_SCOPE_FLAG_*. Together they enabled the persistentcode loader port.
Layout assertion mechanics¶
The asserts run at startup via a comptime block in assertLayout():
pub fn assertLayout() void {
std.debug.assert(@sizeOf(MpObjList) == npy_mirror_sizeof_mp_obj_list_t);
std.debug.assert(@offsetOf(MpObjList, "alloc") == npy_mirror_offsetof_mp_obj_list_t_alloc);
// ... ~50 more
}
npy_mirror_sizeof_mp_obj_list_t is an extern const declared in src/mp_layout_check.zig. Today its value comes from another Zig file (tautological); historically it came from a C TU compiled against vendor headers.
In Debug builds, assertLayout panics on mismatch with a useful message (expected 32, got 24). In Release builds, the std.debug.assert is compiled out; the asserts only fire during development.
This is a defense against drift (rather than against an actual attack). Vendor's struct layout changes only when the upstream tree is updated (via make port), and make port regenerates the C structs we're mirroring. If the regenerated structs differ from the Zig mirror, the asserts catch it before the binary runs corpus tests.
When mirrors fail¶
Most often, a mirror failure looks like:
That's a clue that some struct's len field is at the wrong offset. The fix is:
- Find the corresponding vendor C struct declaration.
- Compare field-for-field with the Zig mirror.
- Adjust the mirror to match.
Common drift causes:
- A vendor #if MICROPY_PY_X block adds a field under some configs and the mirror was written for one config only.
- A vendor refactor moved fields around; the Zig mirror is stale.
- A new mirror was added by hand without @offsetOf asserts.
The assertLayout() call is invoked from main.zig startup. Adding new mirrors should always include their asserts; missing asserts let mismatches sneak through to runtime crashes.
See also¶
src/mp_mirrors.zig(in tree): the canonical sourcesrc/mp_layout_check.zig: the extern const declarationsnotes/19-detailed-design.md: original mirror design rationale