mcap/modules/Basic/module.jai

720 lines
24 KiB
Plaintext

// Simple procedures that many programs may want to use.
#load "Array.jai";
#load "Simple_String.jai";
#load "String_Builder.jai";
#load "Print.jai";
#load "Int128.jai";
#load "Apollo_Time.jai";
#load "string_to_float.jai";
#load "float_to_string.jai";
#load "fixed_arena.jai";
#load "serialization.jai";
#load "arena.jai";
#load "enums.jai";
#load "timing.jai";
#if (OS == .WINDOWS) || (OS == .PS5) || (OS == .NN_SWITCH2) || (OS == .LINUX) || (OS == .MACOS) {
#load "Temporary_Storage_Paged_Alloc.jai"; // @Incomplete: Flat_Pool version.
} else {
#load "Temporary_Storage_Paged_Alloc.jai";
}
// The suites for arena.jai and fixed_arena.jai live with the code they test so they can reach
// its module scope. They export run_arena_tests / run_fixed_arena_tests and nothing else;
// everything they use internally is #scope_file.
#if LOAD_TESTS {
#load "tests/arena_tests.jai";
#load "tests/fixed_arena_tests.jai";
}
#module_parameters () (MEMORY_DEBUGGER := false, ENABLE_ASSERT := true, REPLACEMENT_INTERFACE: $I/interface Memory_Debugger_Interface = Memory_Debugger_Interface, VISUALIZE_MEMORY_DEBUGGER := true, TEMP_ALLOCATOR_OVERWRITE_MEMORY := false, ARENA_DEBUG := false, LOAD_TESTS := false, ENABLE_TIMING := true) {
// Your custom-supplied memory debugger should conform to this interface:
Memory_Debugger_Interface :: struct {
check_alloc :: inline (allocator: Allocator, memory: *void, size: s64) {};
check_free :: inline (allocator: Allocator, memory: *void) {};
check_create_heap :: inline (a: Allocator) {};
check_destroy_heap :: inline (a: Allocator) {};
}
};
// secure_overwrite / memory_secure_overwrite used to live here. arena.jai still takes
// memory_secure_overwrite as its default $wipe_function. Only the main program may supply
// module parameters, so this import gets Secure_Memory at its defaults -- which is why its
// USE_SODIUM defaults to true rather than being something callers switch on.
#import "Secure_Memory";
// VISUALIZE_MEMORY_DEBUGGER and REPLACEMENT_INTERFACE only do anything if MEMORY_DEBUGGER == true.
// ENABLE_TIMING compiles in the tick-based timers in timing.jai.
// TEMP_ALLOCATOR_OVERWRITE_MEMORY memset free memory values to help track down use-after-free issues
SET_STORAGE_MARK_STAMP :: 0xda;
RESET_STORAGE_STAMP :: 0xdb;
FREE_PAGE_STAMP :: 0xdc;
//
// modules/Basic is intended to include a bunch of stuff that most programs will want to do;
// however, it is also supposed to be completely replaceable, so that you don't have to
// run on top of it if you don't want to.
//
// We could definitely use some comprehensive documentation on this module!
//
#if MEMORY_DEBUGGER {
#load "Memory_Debugger.jai";
#if VISUALIZE_MEMORY_DEBUGGER #load "Visualize_Memory_Debugger.jai";
#if REPLACEMENT_INTERFACE == Memory_Debugger_Interface { // Memory_Debugger_Interface is a sentinel; it means the user didn't pass a replacement, so if we see this, we just use our own stuff.
Memory_Debugger :: struct {
check_alloc :: _check_alloc;
check_free :: _check_free;
check_create_heap :: _check_create_heap;
check_destroy_heap :: _check_destroy_heap;
}
} else {
Memory_Debugger :: REPLACEMENT_INTERFACE;
}
} else {
// Declare this routine as a dummy so it can be used throughout the user-level code without #iffing it.
this_allocation_is_not_a_leak :: (memory: *void) #expand #no_debug {}
this_allocation_leaks_todo :: (memory: *void) #expand #no_debug {}
this_allocation_does_not_propagate_leak_ownership :: (memory: *void) #expand #no_debug {}
report_memory_leaks :: () {};
}
#if !(MEMORY_DEBUGGER && VISUALIZE_MEMORY_DEBUGGER) {
memory_visualizer_per_frame_update :: () #expand #no_debug {}
}
get_command_line_arguments :: () -> [] string {
c_args := __command_line_arguments;
result := NewArray(c_args.count, string);
for c_args result[it_index] = to_string(it);
return result;
}
assert_helper :: (message := "", args: .. Any, loc := #caller_location) -> bool {
if context.handling_assertion_failure return false; // Avoid infinite loops.
context.handling_assertion_failure = true;
// We provide 'loc' in case _STACK_TRACE is turned off, but assertion_failed may well
// look at the stack trace if it is available.
should_break := context.assertion_failed(loc, tprint(message, ..args));
context.handling_assertion_failure = false;
return should_break;
}
#if ENABLE_ASSERT {
// We make assert() a macro so that it doesn't annoy you by being on the call stack.
// To keep it short we factor most of the code into assert_helper() (which you could call
// from your own assert implementation, as it is included in the module even if ENABLE_ASSERT
// is false).
assert :: (arg: bool, message := "", args: .. Any, loc := #caller_location) #no_debug #expand {
if !arg && assert_helper(message, ..args, loc) then debug_break();
}
} else {
assert :: (#discard arg: bool, #discard message := "", #discard args: .. Any, #discard loc := #caller_location) #expand {}
// assert :: (arg: bool, message := "", args: .. Any, loc := #caller_location) #expand {}
}
alloc :: (size: s64) -> *void {
a := context.allocator;
result := a.proc(.ALLOCATE, size, 0, null, a.data);
#if MEMORY_DEBUGGER Memory_Debugger.check_alloc(a, result, size);
return result;
}
free :: (memory: *void) {
a := context.allocator;
#if MEMORY_DEBUGGER Memory_Debugger.check_free(a, memory);
a.proc(.FREE, 0, 0, memory, a.data);
}
realloc :: (memory: *void, size: s64, old_size: s64) -> *void {
a := context.allocator;
// We must not just do a "check_realloc" at the end (as previous versions of this function did)
// because that opens us up to race conditions where the memory debugger sees events in a different order that the allocator.
// If thread 1 reallocs some memory which frees address A and then thread 2 reallocs and gets address A
// the memory debugger will report incorrect double-alloc errors if it sees thread 2's realloc before the thread 1's free.
// This solution is not ideal because we will report free+alloc to the memory debugger even if the existing allocation
// was just resized in place.
// The other way to fix this would be to hold the memory debugger lock for the whole duration of the realloc
// to prevent the debugger from seeing events in a different order than the allocator. But this will probably slow down
// allocations too much in a contended multi-threaded scenario, so I opted for this solution.
// -rluba, 2026-06-19
#if MEMORY_DEBUGGER Memory_Debugger.check_free(a, memory);
result := a.proc(.RESIZE, size, old_size, memory, a.data);
#if MEMORY_DEBUGGER Memory_Debugger.check_alloc(a, result, size);
return result;
}
create_heap :: (a: Allocator) -> Allocator {
b_data := a.proc(.CREATE_HEAP, 0, 0, null, a.data);
b := Allocator.{a.proc, b_data};
#if MEMORY_DEBUGGER Memory_Debugger.check_create_heap(b);
return b;
}
destroy_heap :: (b: Allocator) {
#if MEMORY_DEBUGGER Memory_Debugger.check_destroy_heap(b);
b.proc(.DESTROY_HEAP, 0, 0, null, b.data);
}
get_capabilities :: (b: Allocator) -> (caps: Allocator_Caps, name: string) {
name: string;
result := cast,no_check(Allocator_Caps)b.proc(.CAPS, 0, 0, *name, null);
return result, name;
}
// Allocates a string.
alloc_string :: (count: int) -> string {
assert(count >= 0);
if !count return "";
s: string = ---;
s.data = alloc(count);
s.count = count;
return s;
}
talloc_string :: (count: int) -> string { // This is kind of silly now; we might deprecate it.
return inline alloc_string(count,, temp);
}
string_free :: inline (s: string) { free(s.data); }
free :: inline (s: string) { free(s.data); }
push_allocator :: (allocator: Allocator) #expand #no_debug { // This should probably be in caps or something, to tell people it's a macro.
old_allocator := context.allocator;
context.allocator = allocator;
`defer context.allocator = old_allocator;
}
push_allocator :: (proc: Allocator_Proc, data: *void) #expand #no_debug {
old_allocator := context.allocator;
context.allocator.proc = proc;
context.allocator.data = data;
`defer context.allocator = old_allocator;
}
// Should get_field not be in Basic? Do we use this any more?
get_field :: (info: *Type_Info_Struct, name: string) -> *Type_Info_Struct_Member, offset_in_bytes: s64 {
// First pass: Don't try recursing.
for * info.members {
if name == it.name return it, it.offset_in_bytes;
}
// Second pass: See if we can get something recursively.
for * info.members {
if (it.flags & .USING) && (it.type.type == .STRUCT) {
result, child_offset := get_field(cast(*Type_Info_Struct)it.type, name);
if result return result, it.offset_in_bytes + child_offset;
}
}
return null, 0;
}
min :: (a: $T, b: T) -> T { // Return the smallest of (a, b).
if a < b return a;
return b;
}
max :: (a: $T, b: T) -> T { // Return the largest of (a, b).
if b < a return a;
return b;
}
// In case you shadow 'min' and 'max' with local variables, provide capitalized versions...?
// I had originally made these capitalized until someone passive-aggressively changed them.
// Change them back? -jblow, 2 June 2021.
Min :: min;
Max :: max;
IsScalar :: (T: Type) -> bool #compile_time { // This is a utility used in the #modify routines for max, min, clamp.
ti := cast(*Type_Info) T;
if ti.type == {
// These are fine:
case .INTEGER; #through;
case .FLOAT; #through;
case .POINTER; #through;
case .ENUM;
return true;
}
return false;
}
max :: inline (a: $T, x: ..T) -> T
#modify { return IsScalar(T), "Argument is not of a scalar type."; }
{
m := a;
for x if m < it m = it;
return m;
}
min :: inline (a: $T, x: ..T) -> T
#modify { return IsScalar(T), "Argument is not of a scalar type."; }
{
m := a;
for x if it < m m = it;
return m;
}
clamp :: (x: $T, a: T, b: T) -> T
#modify { return IsScalar(T), "Argument is not of a scalar type."; }
{
if x < a x = a;
if x > b x = b;
return x;
}
swap :: inline (a: *$T, b: *T) {
// This is for when only want to mention each expression once instead of twice, because the names are long...
a.*, b.* = b.*, a.*;
}
// @Cleanup: We would like to do:
//
// talloc :: temporary_alloc;
//
// But this makes the code live right now. When we fix this part of the
// dead code elimination, we can do that...
talloc :: inline (size: s64) -> *void {
return temporary_allocator_proc(.ALLOCATE, size, 0, null, null);
}
temporary_alloc :: inline (size: s64) -> *void {
return temporary_allocator_proc(.ALLOCATE, size, 0, null, null);
}
temp :: Allocator.{temporary_allocator_proc, null};
temporary_allocator :: temp;
reset_temporary_storage :: () {
ts := context.temporary_storage;
if ts reset_temporary_storage(ts);
}
round :: (f: float32) -> s32 #expand {
return cast(s32) floor(f+0.5);
}
reset_struct :: Initialize;
Initialize :: (memory: *$T) {
ini :: initializer_of(T);
#if ini inline ini(memory);
else memset(memory, 0, size_of(T));
}
New :: ($T: Type, $initialized := true) -> *T {
memory := alloc(size_of(T));
// Since New gets called a lot, we just roll Initialize() into here, so that
// we don't kick off so many extra polymorphs.
#if initialized {
ini :: initializer_of(T);
#if ini inline ini(memory);
else memset(memory, 0, size_of(T));
}
return cast(*T) memory;
}
NewArray :: (count: s64, $T: Type, $initialized := true, alignment: s32 = -1) -> (result: [] T, unaligned_base: *void) {
if !count return .[], null;
// NewArray is for when we want to new a not-known-at-compile-time
// number of things...
a := context.allocator;
extra: s32 = 0;
if alignment > 0 {
extra = alignment;
}
// 'extra' lets us allocate extra memory so that the user can bump the pointer forward
// for alignment purposes.
// This is more versatile than requiring all allocators to support wacky alignments.
// This doesn't mean it's the best solution, but it is what we are doing for now.
original_memory := cast(*void) alloc(size_of(T) * count + extra);
if !original_memory return .[], null;
memory: *void;
if alignment > 0 {
memory = cast(*void) align_forward(cast(s64)original_memory, alignment);
} else {
memory = original_memory;
}
result: [] T = ---;
result.count = count;
result.data = memory;
#if initialized {
ini :: initializer_of(T);
#if ini {
for 0..count-1 {
inline ini(memory);
memory += size_of(T);
}
} else {
memset(memory, 0, count * size_of(T));
}
}
// If alignment == -1, you don't need to think about original_memory; you can just ignore it.
// You only need to care if alignment != -1; in that case, when it comes time to free the memory,
// you should free original_memory, not result.data.
return result, original_memory;
}
decimal_digits :: (v_in: s32) -> s32 {
v := v_in;
if (v < 0) v = -v; // ignore sign
if (v < 10) return 1;
if (v < 100) return 2;
if (v < 1000) return 3;
if (v < 10000) return 4;
if (v < 100000) return 5;
if (v < 1000000) return 6;
if (v < 10000000) return 7;
if (v < 100000000) return 8;
if (v < 1000000000) return 9;
return 10; // max for s32 (2,147,483,648)
}
// align_forward adds to orig_size so that it becomes an even multiple of 'alignment'.
align_forward :: (orig_size: s64, alignment: s64) -> s64 {
a := alignment;
assert(a > 0);
size := ((orig_size + a - 1) / a) * a;
// The above code might be hard to think about. Here's another implementation,
// that has a branch:
// size := orig_size;
// remainder := orig_size % a;
// if remainder size += a - remainder;
return size;
}
#if OS == {
case .WINDOWS; #load "windows.jai";
case .LINUX; #load "linux.jai";
case .ANDROID; #load "linux.jai";
case .MACOS; #load "osx.jai";
case .PS5; #load "ps5.jai";
case .NN_SWITCH; #load "nintendo.jai";
case .NN_SWITCH2; #load "nintendo.jai";
}
// pack_stack_trace is convenient in case you want to store the stack trace
// on a data structure. You can't just store a pointer to context.stack_trace,
// because that data will be destroyed as the stack unwinds. So this routine
// copies that trace into a packed array.
pack_stack_trace :: () -> [] Stack_Trace_Node {
return pack_stack_trace(context.stack_trace);
}
pack_stack_trace :: (node: *Stack_Trace_Node) -> [] Stack_Trace_Node {
// We could do a two-pass version where we array_reserve the right amount of data...!
result: [..] Stack_Trace_Node;
while node {
array_add(*result, node.*);
node = node.next;
}
// Fix up the 'next' pointers so you can use them normally.
for * result {
if it.next // The one that was already null, at the end, leave it null. Otherwise:
it.next = it + 1; // Set 'next' to point at the subsequent element in the array.
}
return result;
}
//
// print_stack_trace() can be used as a low-level routine for people writing
// Allocators, assertion handlers, loggers etc, and who are encountering
// bugs that they want help with. This is guaranteed not to call into
// any logger, allocator, etc because it uses only bottom-level routines.
//
// That said, you can also use it in other code!
//
//
// Higher-level routines can use log_stack_trace(), or get_stack_trace_strings(), which
// gives you strings you can render graphically, etc.
//
print_stack_trace :: (node: *Stack_Trace_Node, prefix: string = "", to_standard_error := true) {
while node {
if node.info {
if prefix.count write_string(prefix, to_standard_error = to_standard_error);
write_string("'", to_standard_error = to_standard_error);
if node.info.name write_string(node.info.name, to_standard_error = to_standard_error);
write_string("' at ", to_standard_error = to_standard_error);
if node.info.location.fully_pathed_filename write_string(node.info.location.fully_pathed_filename, to_standard_error = to_standard_error);
write_string(":", to_standard_error = to_standard_error);
write_nonnegative_number(node.line_number, to_standard_error = to_standard_error);
write_string("\n", to_standard_error = to_standard_error);
}
node = node.next;
}
}
log_stack_trace :: (node: *Stack_Trace_Node = null, log_flags := Log_Flags.ERROR, prefix := "") {
strings := get_stack_trace_strings(node);
for strings log("%1%2\n", prefix, it, flags = log_flags);
}
get_stack_trace_strings :: (node: *Stack_Trace_Node = null) -> [..] string /* Temporary_Storage */ {
if !node node = context.stack_trace;
results: [..] string;
results.allocator = temp;
while node {
if node.info {
s := sprint("'%' at %:%", node.info.name, node.info.location.fully_pathed_filename, node.line_number,, allocator=temp);
array_add(*results, s);
}
node = node.next;
}
return results;
}
remember_allocators :: (data: *$T) {
// In this language, we want the user of a function or library to be able to
// control that function's memory allocation using context.allocator.
//
// When you make a data structure that needs to allocate memory over a long
// period of time, though, this introduces a potential annoying constraint:
// if the user wants to store that data structure or pass it to some other code,
// that the user did not write, that other code will not know what allocator to push.
// So it's a good idea for data structures like this to store allocators at
// initialization time. (Data structures like Hash_Table and Bucket_Array do this;
// this is also why the language-native [..] array has allocator and allocator_data
// slots.)
//
// To make it convenient to save allocators in this way, we provide this function;
// it's a very simple function, but it helps people remember and continue the convention.
// All you need is to have 'allocator' and 'allocator_data' members, then call
// this routine.
if context.allocator.proc {
data.allocator = context.allocator;
} else {
data.allocator = context.default_allocator;
}
}
resizable :: (a: [] $T, set_allocators := true) -> [..] T {
// Convert a [] to a [..] that owns the same memory.
// The [..]'s allocated pointer will be set to a.count, so it will
// reallocate if you try to add any more.
// Since there is no way to know what the allocator of the [] was,
// we assume it's the current allocator, and remember that, unless you
// say set_allocators=false.
result: [..] T = ---;
result.count = a.count;
result.allocated = a.count;
result.data = a.data;
if set_allocators remember_allocators(*result);
return result;
}
get_integer_range :: ($T: Type) -> (low: T, high: T) {
// Returns the lowest and highest values for a given integer type.
// For example, u8 returns (0, 255); s8 returns (-127, 128).
IS_INTEGER, SIGNED, BITS :: #run is_integer_type(T);
#assert(IS_INTEGER);
#if SIGNED {
low := (cast(T)1) << (BITS-1);
} else {
low: T = 0;
}
return low, ~low;
}
clamp_to_another_integer_type :: (value: $SourceType, $TargetType: Type) -> TargetType {
// SourceType and TargetType
SOURCE_INTEGER, SOURCE_SIGNED, SOURCE_BITS :: #run is_integer_type(SourceType);
TARGET_INTEGER, TARGET_SIGNED, TARGET_BITS :: #run is_integer_type(TargetType);
#assert SOURCE_INTEGER && TARGET_INTEGER;
LOW, HIGH :: #run get_integer_range(TargetType);
#if SOURCE_SIGNED == TARGET_SIGNED {
// Both are signed, or both are unsigned.
#if TARGET_BITS < SOURCE_BITS {
if value < LOW value = LOW;
if value > HIGH value = HIGH;
}
return cast,no_check(TargetType) value;
} else {
// If we get here, SOURCE_SIGNED != TARGET_SIGNED.
#if SOURCE_SIGNED {
// TARGET_SIGNED == false.
if value < 0 value = 0;
#if TARGET_BITS < SOURCE_BITS {
if value > HIGH value = HIGH;
}
} else {
// SOURCE_SIGNED == false, TARGET_SIGNED == true.
#if TARGET_BITS <= SOURCE_BITS {
if value > HIGH value = HIGH;
}
}
return cast,no_check(TargetType) value;
}
}
is_integer_type :: ($T: Type) -> (is_integer: bool, is_signed: bool, bits: u8) {
info := type_info(T);
if info.type == .INTEGER {
info_int := cast(*Type_Info_Integer) info;
return true, info_int.signed, size_of(T)*8;
}
return false, false, 0;
}
seconds_since_init :: () -> float64 {
// This is the replacement for seconds_since_init().
one_time_init(*is_get_seconds_initialized, init_get_seconds());
delta := current_time_monotonic() - get_seconds_base_time;
return to_float64_seconds(delta);
}
#scope_file
is_get_seconds_initialized: s32;
get_seconds_base_time: Apollo_Time;
init_get_seconds :: () {
get_seconds_base_time = current_time_monotonic();
}
#scope_export
// Ok, so for many years kilo, mega and gigabytes meant 1024 * ..., but now
// we're supposed to use kibi, mibi and gibi??
Bytes :: (b: s64) -> s64 #expand { return b; }
KB :: Kilobytes;
Kilobytes :: (count: s64) -> s64 #expand { return count * 1024; }
MB :: Megabytes;
Megabytes :: (count: s64) -> s64 #expand { return count * 1_048_576; }
GB :: Gigabytes;
Gigabytes :: (count: s64) -> s64 #expand { return count * 1_073_741_824; }
TB :: Terabytes;
Terabytes :: (count: s64) -> s64 #expand { return count * 1_099_511_627_776; }
format_time_milliseconds :: (time: float64) -> string {
return tprint("% ms", FF(time * 1.0e3, 3));
}
format_delta_seconds :: inline (end_time: Apollo_Time, start_time: Apollo_Time) -> string {
return format_time_seconds(to_float64_seconds(end_time-start_time));
}
format_time_seconds :: (time: float64) -> string {
if time < 1.0e-6 then return tprint("% ns", FF(time * 1.0e9, 2));
if time < 1.0e-3 then return tprint("% us", FF(time * 1.0e6, 3));
if time < 1 then return tprint("% ms", FF(time * 1.0e3, 3));
return tprint("% s", FF(time, 2));
}
format_count :: (count: $T) -> string {
units : []string = .["", "thousand", "million", "billion", "trillion"];
count_f64 := cast(float64)count;
unit_index := 0;
while count_f64 >= 1000 && unit_index < (units.count - 1) {
count_f64 /= 1000.0;
unit_index += 1;
}
return tprint("% %", FF(count_f64, 2), units[unit_index]);
}
format_cycles :: (ticks: $T) -> string {
units : []string = .["cycles", "K cycles", "M cycles", "B cycles", "T cycles"];
count_f64 := cast(float64)ticks;
unit_index := 0;
while count_f64 >= 1000 && unit_index < (units.count - 1) {
count_f64 /= 1000.0;
unit_index += 1;
}
return tprint("% %", FF(count_f64, 2), units[unit_index]);
}
format_bytes :: (bytes_count: int) -> string {
units : []string = .["B", "KB", "MB", "GB", "TB", "PB"];
count_f64 := cast(float64)bytes_count;
unit_index := 0;
// Scale down until size < 1024 or we run out of units
while count_f64 >= 1024 && unit_index < (units.count - 1) {
count_f64 /= 1024.0;
unit_index += 1;
}
return tprint("% %", FF(count_f64, 2), units[unit_index]);
}