tiny.game.reload
Defined in tiny.game.
A loader that runs a game from a shared library and swaps in the rebuilt library while the host keeps running.
API (2)
Actions
Public operations.
Loader: Returns a loader type for game libraries that follow the interfaceAbidescribes, withinlimits.
Types and contracts
Public types and contracts.
Limits: The largest state alignment and the size bound for the library file the loader copies, fixed when the loader type is made.
Source
Source: fun/game/src/reload.zig
zig
//! A loader that runs a game from a shared library and swaps in the rebuilt library while the host//! keeps running.//!//! A developer who rebuilds the game's code wants the host to pick up the change while it keeps//! running, with the game's state kept in the host's memory.//!//! A compiler writes a library file over a stretch of time, so a host that loads the file at its//! first change can load a half-written library. Rebuilt code takes over the game state the old//! code left in memory, so the rebuilt code has to fit that memory and agree with the host on how//! the two call each other.//!//! The loader copies the library file to a new numbered file beside it (a *staged copy*) and opens//! the copy. At each poll the loader looks at the original file, and once the file's size and//! modification time have held still for 120 milliseconds, it stages and opens a fresh copy. Before//! it swaps, the loader calls a function the library exports and reads back a record that describes//! the library (a *contract*), and it checks the major interface version and the alignment of the//! state the library needs. The loader also checks the state size against a limit//! (`state_capacity`) that starts at the first library's state size and that a caller can set, for//! example to the size of the state memory the host allocated. The caller fixes the largest state//! alignment and the size bound for the library file the loader copies when it makes the loader//! type (`Limits`). After a refusal, the old library stays loaded and the poll's result carries the//! error, and after a swap the loader deletes the previous copy and the poll's result carries the//! new contract.const std = @import("std");const sys = @import("sys");const settle = @import("settle.zig");const settle_window_ns: i128 = 120 * std.time.ns_per_ms;/// The largest state alignment and the size bound for the library file the loader copies, fixed/// when the loader type is made. A host picks both once for its loader type, for example an/// alignment of 16 bytes and a file of 64 MiB. Making the loader type is a compile error when/// either limit is zero or the alignment fails to be a power of two.pub const Limits = struct { /// The largest state alignment in bytes that a library may ask for, a power of two. A host /// allocates its state memory at this alignment, so the state of any accepted library fits it. max_state_align: usize, /// The size bound in bytes for the library file the loader copies. A file of this size or /// larger is refused with `error.StreamTooLong`, so the largest accepted file is one byte /// smaller. max_library_bytes: usize,};const ReloadError = sys.dynamic.LibraryError || sys.fs.ReadFileError || sys.fs.WriteFileError || std.mem.Allocator.Error || error{ MissingContract, ContractTooNew, ContractAbiMismatch, InvalidStateAlignment, StateLayoutChanged,};/// Returns a loader type for game libraries that follow the interface `Abi` describes, within/// `limits`. A host makes one loader type for its game interface and keeps one loader for the game/// it runs. `Abi` declares the record type `Contract`, the type of the function that fills it/// `ContractFn`, that function's exported name `contract_symbol`, and the host's major interface/// version `current_abi_major`. `Contract` has at least the fields `abi_major`, `state_size` and/// `state_align`. A loader holds the open library, its contract, its staged copy, and a change/// detector for the original file. Each staged copy's path is the original path with `.hot.` and a/// number appended, and the first load uses 0.pub fn Loader(comptime Abi: type, comptime limits: Limits) type { comptime { if (limits.max_state_align == 0 or !std.math.isPowerOfTwo(limits.max_state_align) or limits.max_library_bytes == 0) @compileError("invalid game reload limits"); } return struct { const Dynamic = @This(); /// Every error the loader returns: the errors of opening a library, reading the file and /// writing the copy, running out of memory, and five contract errors. A host names it in /// its own signatures and reports a refused swap by the error's name. `MissingContract`: /// the library exports no function by the contract name. `ContractTooNew`: the library's /// function returned 0 for the host's record size, which a library does when its own record /// is larger. `ContractAbiMismatch`: the major interface version differs from /// `current_abi_major`. `InvalidStateAlignment`: the state alignment is zero, fails to be a /// power of two, or is above `max_state_align`. `StateLayoutChanged`: a new library's state /// size is above `state_capacity`. `StateLayoutChanged` compares sizes alone, so a library /// whose state layout changes at the same or a smaller size is accepted. pub const Error = ReloadError; /// The result of one `poll`: the loader stayed idle, swapped in a rebuilt library, or /// refused a swap. A host switches on it after each poll, takes the new contract after a /// swap, and reports a refusal. pub const Poll = union(enum) { /// The loader stayed idle: the file is unchanged or still settling, or the look at its /// size and modification time returned an error. idle, /// The loader swapped in a rebuilt library, and the payload is its contract. The old /// library is closed by then, so the caller replaces every contract copy it holds with /// this one. swapped: Abi.Contract, /// The file settled and the swap was refused, and the payload is the error. Staging the /// copy, before any copy is opened, returns the errors of reading the library file, /// `error.StreamTooLong` for a file at the size bound or larger, `error.OutOfMemory`, /// and the errors of writing the copy. Opening the fresh copy returns the errors of /// opening a library, and reading and checking its contract returns the contract /// errors. The old library, its contract and its staged copy stay in place. The loader /// tries again only after the file changes and settles again. failed: ReloadError, }; allocator: std.mem.Allocator, source_path: []u8, staged_path: ?[]u8, lib: sys.dynamic.Library, /// The contract of the library that is loaded now. A host reads it after `load` for the /// game's functions and its state size. contract: Abi.Contract, settle: settle.Settle, counter: usize, /// The largest state size in bytes that a rebuilt library may ask for. `load` sets it to /// the first library's state size, and a caller can set it, for example to the size of the /// state memory the host allocated. state_capacity: usize, /// Copies the library at `path` to its first staged copy, opens the copy and reads its /// contract. A host calls it once at startup with the path of the game's library. The /// loader keeps its own copy of `path` and allocates it and the staged path with /// `allocator`, and `deinit` frees both. The call records the file's size and modification /// time as the change detector's first look when it can read them. The call returns the /// errors of opening, reading and writing, `error.OutOfMemory`, and each contract error /// except `StateLayoutChanged`. On an error, the call deletes the staged copy it opened and /// frees what it allocated. When writing the copy fails after the copy's file exists, the /// partly written copy stays on disk until a later `load` of the same path writes its copy /// under the same name. A state size of zero is accepted, for a stateless game. pub fn load(allocator: std.mem.Allocator, path: []const u8) ReloadError!Dynamic { const source_path = try allocator.dupe(u8, path); errdefer allocator.free(source_path); const staged = try stage(allocator, source_path, 0); errdefer { sys.fs.deleteFile(staged) catch {}; allocator.free(staged); } var lib = try sys.dynamic.openRuntimeFfiPlugin(staged); errdefer lib.close(); const contract = try readContract(&lib); var watch = settle.Settle{}; if (sys.fs.statFile(source_path)) |st| { _ = watch.observe(.{ .mtime_ns = st.mtime.nanoseconds, .size = st.size }, 0, settle_window_ns); } else |_| {} return .{ .allocator = allocator, .source_path = source_path, .staged_path = staged, .lib = lib, .contract = contract, .settle = watch, .counter = 1, .state_capacity = contract.state_size, }; } /// Looks at the library file and swaps in a fresh copy once the file has held still for 120 /// milliseconds. A host calls it once per step before it runs the game's update, passing /// the current time in nanoseconds. A swap stages the next numbered copy, opens it, reads /// and checks its contract, and checks its state size against `state_capacity`. A swap then /// closes the old library, deletes its staged copy, and keeps the new library and contract. /// A refused swap deletes the new copy and leaves the old library, its contract and its /// staged copy in place. When writing the new copy fails after the copy's file exists, the /// partly written copy stays on disk until the next swap writes its copy under the same /// number, and `deinit` leaves it there. The result is `.idle`, `.swapped` with the new /// contract, or `.failed` with the error. The call returns no error of its own: a failure /// to read the file's size and time gives `.idle`, and a swap error gives `.failed`. pub fn poll(self: *Dynamic, now_ns: i128) Poll { const st = sys.fs.statFile(self.source_path) catch return .idle; if (!self.settle.observe(.{ .mtime_ns = st.mtime.nanoseconds, .size = st.size }, now_ns, settle_window_ns)) return .idle; const next = self.swap() catch |err| return .{ .failed = err }; return .{ .swapped = next }; } /// Closes the library, deletes the current staged copy, frees the loader's paths, and /// leaves the loader undefined. A host calls it once at shutdown, after its last call into /// the game. The function pointers in any contract from this loader are invalid after the /// call. pub fn deinit(self: *Dynamic) void { self.lib.close(); if (self.staged_path) |p| { sys.fs.deleteFile(p) catch {}; self.allocator.free(p); } self.allocator.free(self.source_path); self.* = undefined; } fn swap(self: *Dynamic) ReloadError!Abi.Contract { const staged = try stage(self.allocator, self.source_path, self.counter); errdefer { sys.fs.deleteFile(staged) catch {}; self.allocator.free(staged); } self.counter += 1; var new_lib = try sys.dynamic.openRuntimeFfiPlugin(staged); errdefer new_lib.close(); const next = try readContract(&new_lib); if (next.state_size > self.state_capacity) { return error.StateLayoutChanged; } self.lib.close(); if (self.staged_path) |old| { sys.fs.deleteFile(old) catch {}; self.allocator.free(old); } self.lib = new_lib; self.staged_path = staged; self.contract = next; return next; } fn stage(allocator: std.mem.Allocator, source_path: []const u8, counter: usize) ReloadError![]u8 { const staged = try std.fmt.allocPrint(allocator, "{s}.hot.{d}", .{ source_path, counter }); errdefer allocator.free(staged); const bytes = try sys.fs.readFileAlloc(allocator, source_path, limits.max_library_bytes); defer allocator.free(bytes); try sys.fs.writeFile(staged, bytes); return staged; } fn readContract(lib: *sys.dynamic.Library) ReloadError!Abi.Contract { const entry = lib.lookup(Abi.ContractFn, Abi.contract_symbol) orelse return error.MissingContract; var contract = std.mem.zeroes(Abi.Contract); if (entry(&contract, @sizeOf(Abi.Contract)) == 0) return error.ContractTooNew; if (contract.abi_major != Abi.current_abi_major) return error.ContractAbiMismatch; if (contract.state_align == 0 or !std.math.isPowerOfTwo(contract.state_align) or contract.state_align > limits.max_state_align) return error.InvalidStateAlignment; return contract; } };}Source: fun/game/src/root.zig:119
zig
pub const reload = @import("reload.zig");Audit
| Definitions | 3 |
|---|---|
| Public names | 3 |
| Members | 2 |
| Version | 26.7.0 |
| Revision | daab053ee433 |