//! The user-space heap: C-convention dynamic allocation (`malloc`/`free`/…) plus //! a `std.mem.Allocator` adapter over the same free list, so both C-style code //! and Zig `std` containers share one heap. //! //! The algorithm is a straight port of the kernel's first-fit free list //! (system/kernel/heap.zig): an address-ordered singly linked list of free blocks, //! split on allocation and coalesced with neighbours on free. The only thing //! that changes on this side of the system_call boundary is where memory comes from //! — `grow` asks the kernel for pages via `mmap` instead of mapping frames //! itself, and the kernel picks the base address. //! //! Single-threaded and 16-byte maximum alignment, exactly like the kernel heap; a //! lock and larger alignments come when user programs gain threads. const std = @import("std"); const abi = @import("abi"); const system_calls = @import("system.zig"); const page_size = abi.page_size; /// A block header, at the start of every block; while free it also links the /// free list via `next`. const Block = extern struct { size: usize, // total block size in bytes, including this header; a multiple of 16 next: ?*Block, // free-list link (only meaningful while free) }; const header_size = @sizeOf(Block); // 16 const minimum_block = header_size + 16; // smallest block worth splitting off /// Grow granularity: one `mmap` per 64 KiB amortises the system_call. const chunk = 64 * 1024; var free_list: ?*Block = null; fn alignUp(value: usize, alignment: usize) usize { return (value + alignment - 1) & ~(alignment - 1); } fn payloadOf(block: *Block) [*]u8 { return @ptrFromInt(@intFromPtr(block) + header_size); } /// Ask the kernel for more pages and add them as a free block. Because each /// `mmap` is an independent grant, cross-grant coalescing happens only when the /// kernel returns adjacent bases (its arena is a bump allocator, so consecutive /// grants usually are adjacent). Returns false if the kernel is out of memory. fn grow(minimum_bytes: usize) bool { const bytes = alignUp(@max(minimum_bytes, chunk), page_size); const ret = system_calls.mmap(bytes, system_calls.PROT_READ | system_calls.PROT_WRITE); if (system_calls.mmapFailed(ret)) return false; const block: *Block = @ptrFromInt(ret); block.size = bytes; insertFree(block); // coalesces if this grant is adjacent to a prior one return true; } /// Insert a block into the address-ordered free list, coalescing with the /// physically adjacent free blocks on either side. fn insertFree(block: *Block) void { var previous: ?*Block = null; var current = free_list; while (current) |c| : (current = c.next) { if (@intFromPtr(c) > @intFromPtr(block)) break; previous = c; } block.next = current; if (previous) |p| p.next = block else free_list = block; // Merge forward into `current` if they're contiguous. if (current) |c| { if (@intFromPtr(block) + block.size == @intFromPtr(c)) { block.size += c.size; block.next = c.next; } } // Merge `previous` forward into `block` if they're contiguous. if (previous) |p| { if (@intFromPtr(p) + p.size == @intFromPtr(block)) { p.size += block.size; p.next = block.next; } } } /// Allocate `len` bytes (16-byte aligned), or null if out of memory. fn rawAlloc(len: usize) ?[*]u8 { const need = alignUp(header_size + len, 16); var attempts: u32 = 0; while (attempts < 2) : (attempts += 1) { var previous: ?*Block = null; var current = free_list; while (current) |block| : ({ previous = block; current = block.next; }) { if (block.size < need) continue; if (block.size >= need + minimum_block) { // Split: carve `need` off the front, leave the rest free. const rest: *Block = @ptrFromInt(@intFromPtr(block) + need); rest.size = block.size - need; rest.next = block.next; if (previous) |p| p.next = rest else free_list = rest; block.size = need; } else { // Take the whole block. if (previous) |p| p.next = block.next else free_list = block.next; } return payloadOf(block); } // Nothing fit: grow and try once more. if (!grow(need)) return null; } return null; } fn rawFree(ptr: [*]u8) void { const block: *Block = @ptrFromInt(@intFromPtr(ptr) - header_size); insertFree(block); } // --- C ABI: the global implicit heap --------------------------------------- // `extern "C"` symbols so future C code links the same malloc/free directly. export fn malloc(size: usize) callconv(.c) ?*anyopaque { if (size == 0) return null; const p = rawAlloc(size) orelse return null; return @ptrCast(p); } export fn free(ptr: ?*anyopaque) callconv(.c) void { const p = ptr orelse return; rawFree(@ptrCast(p)); } export fn calloc(nmemb: usize, size: usize) callconv(.c) ?*anyopaque { const total = std.math.mul(usize, nmemb, size) catch return null; // overflow-safe if (total == 0) return null; const p = rawAlloc(total) orelse return null; @memset(p[0..total], 0); return @ptrCast(p); } export fn realloc(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque { const p = ptr orelse return malloc(size); if (size == 0) { rawFree(@ptrCast(p)); return null; } const block: *Block = @ptrFromInt(@intFromPtr(p) - header_size); const old_payload = block.size - header_size; if (size <= old_payload) return p; // shrink/same: keep the block const np = rawAlloc(size) orelse return null; // grow: alloc + copy + free @memcpy(np[0..old_payload], @as([*]u8, @ptrCast(p))[0..old_payload]); rawFree(@ptrCast(p)); return @ptrCast(np); } // --- std.mem.Allocator interface (same free list) -------------------------- pub fn allocator() std.mem.Allocator { return .{ .ptr = undefined, .vtable = &vtable }; } const vtable = std.mem.Allocator.VTable{ .alloc = allocImpl, .resize = resizeImpl, .remap = remapImpl, .free = freeImpl, }; fn allocImpl(_: *anyopaque, len: usize, alignment: std.mem.Alignment, _: usize) ?[*]u8 { if (alignment.toByteUnits() > 16) return null; // blocks are 16-byte aligned return rawAlloc(len); } fn resizeImpl(_: *anyopaque, memory: []u8, _: std.mem.Alignment, new_len: usize, _: usize) bool { // In-place iff the new payload still fits the current block. const block: *Block = @ptrFromInt(@intFromPtr(memory.ptr) - header_size); return new_len + header_size <= block.size; } fn remapImpl(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) ?[*]u8 { return null; } fn freeImpl(_: *anyopaque, memory: []u8, _: std.mem.Alignment, _: usize) void { rawFree(memory.ptr); }