Skip to documentation
SLOP

tiny.game.reload

Reference 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.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.gamereload
Static calls · unresolved targets: unknown · external targets: unknown.

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

Definitions3
Public names3
Members2
Version26.7.0
Revisiondaab053ee433