844 lines
33 KiB
Plaintext
844 lines
33 KiB
Plaintext
//
|
||
// Runtime_Support is less volatile than Preload and contains imports and various biggish code;
|
||
// it is considered reasonable that you can mess around with Runtime_Support and ship your own
|
||
// version of it.
|
||
//
|
||
|
||
#module_parameters(DEFINE_SYSTEM_ENTRY_POINT: bool, DEFINE_INITIALIZATION: bool, ENABLE_BACKTRACE_ON_CRASH: bool);
|
||
|
||
#scope_export
|
||
|
||
OS_IS_UNIX :: OS == .MACOS || OS == .LINUX || OS == .PS5 || OS == .IOS || OS == .ANDROID;
|
||
|
||
// @Cleanup: __element_duplicate should lose its c_call status, and also be faster...!
|
||
__element_duplicate :: (start: *u8, num_elements: s64, size: s64) #c_call #no_aoc {
|
||
cursor := start + size;
|
||
for 1..num_elements-1 {
|
||
memcpy(cursor, start, size);
|
||
cursor += size;
|
||
}
|
||
}
|
||
|
||
write_nonnegative_number :: (n: u64, base := 10, to_standard_error := false) #no_context #no_abc #no_aoc {
|
||
if n == 0 {
|
||
write_string("0", to_standard_error);
|
||
return;
|
||
}
|
||
|
||
if base > 16 base = 16;
|
||
if base < 2 base = 2;
|
||
|
||
ubase := cast,no_check(u64) base;
|
||
|
||
// This is of course a horrible hack.
|
||
// A u64 in base 2 can be up to 64 digits,
|
||
// so we need at least that much space. Add a few more digits for superstition.
|
||
buf: [69] u8;
|
||
|
||
starting_cursor : s32 = buf.count;
|
||
cursor := starting_cursor;
|
||
|
||
|
||
while n {
|
||
cursor -= 1;
|
||
index := n % ubase;
|
||
if index <= 9 {
|
||
buf[cursor] = cast(u8)(index + #char "0");
|
||
} else {
|
||
buf[cursor] = cast(u8)(index - 10 + #char "a");
|
||
}
|
||
|
||
n /= ubase;
|
||
}
|
||
|
||
s: string = ---;
|
||
s.data = buf.data + cursor;
|
||
s.count = starting_cursor - cursor;
|
||
write_string(s, to_standard_error);
|
||
}
|
||
|
||
write_number :: (n: s64, base := 10, to_standard_error := false) #no_context #no_aoc {
|
||
if n < 0 {
|
||
write_string("-", to_standard_error);
|
||
n = -n; // Will overflow if this is the most negative s64.
|
||
}
|
||
|
||
write_nonnegative_number(cast,no_check(u64) n, base, to_standard_error = to_standard_error);
|
||
}
|
||
|
||
runtime_support_assertion_failed :: (loc: Source_Code_Location, message: string) -> bool {
|
||
write_loc(loc, to_standard_error = true);
|
||
write_string(": Assertion failed", to_standard_error = true);
|
||
|
||
if message {
|
||
write_strings(": ", message, "\n", to_standard_error = true);
|
||
} else {
|
||
write_string("!\n", to_standard_error = true);
|
||
}
|
||
|
||
print_stack_trace :: (node: *Stack_Trace_Node) { // @Copypasta from modules/Basic, but without calling print.
|
||
while node {
|
||
// There are two different line numbers available.
|
||
// node.info.location has a line number, but this is the location of
|
||
// the start of the procedure. That may be useful in some cases, but often
|
||
// what we really want is the line number of the procedure call, which is
|
||
// stored in node.line_number. That's what we print in this case.
|
||
|
||
if node.info {
|
||
write_string(node.info.location.fully_pathed_filename, to_standard_error = true);
|
||
write_string(":", to_standard_error = true);
|
||
write_nonnegative_number(node.line_number, to_standard_error = true);
|
||
write_string(": ", to_standard_error = true);
|
||
if node.info.name write_string(node.info.name, to_standard_error = true);
|
||
else write_string("(anonymous procedure)", to_standard_error = true);
|
||
write_string("\n", to_standard_error = true);
|
||
}
|
||
|
||
node = node.next;
|
||
}
|
||
}
|
||
|
||
if context.stack_trace {
|
||
write_string("\nStack trace:\n", to_standard_error = true);
|
||
print_stack_trace(context.stack_trace);
|
||
} else {
|
||
write_string("Exiting.\n", to_standard_error = true);
|
||
}
|
||
|
||
report_failed_assertion_more_visibly(loc, message);
|
||
|
||
__runtime_support_disable_stack_trace = true;
|
||
|
||
return true;
|
||
}
|
||
|
||
runtime_support_default_logger :: (message: string, data: *void, info: Log_Info) {
|
||
if !message then return;
|
||
|
||
to_standard_error := (info.common_flags & .ERROR) != 0;
|
||
|
||
if message[message.count-1] != #char "\n" {
|
||
write_strings(message, "\n", to_standard_error = to_standard_error);
|
||
} else {
|
||
write_string(message, to_standard_error = to_standard_error);
|
||
}
|
||
}
|
||
|
||
c_style_strlen :: (str: *u8) -> s64 #no_context {
|
||
p := str;
|
||
while p.* { p += 1; }
|
||
return p - str;
|
||
}
|
||
|
||
to_string :: (c_string: *u8) -> string #no_context {
|
||
if c_string == null return "";
|
||
|
||
s: string = ---;
|
||
s.data = c_string;
|
||
s.count = c_style_strlen(c_string);
|
||
|
||
return s;
|
||
}
|
||
|
||
// @Incomplete: Pass loc to Runtime_Support versions.
|
||
__array_bounds_check_fail :: (index: s64, limit: s64, line_number: s64, filename: *u8) #no_context {
|
||
// @Volatile: It is a good idea for these to match the error reports in constant-expression evaluation inside the compiler.
|
||
write_string("Array bounds check failed. (The attempted index is ", to_standard_error = true);
|
||
write_number(index, to_standard_error = true);
|
||
|
||
if limit > 0 {
|
||
write_string(", but the highest valid index is ", to_standard_error = true);
|
||
write_number(limit-1, to_standard_error = true);
|
||
} else if limit == 0 {
|
||
write_string(", but the array has dimension 0, so no index is valid", to_standard_error = true);
|
||
} else {
|
||
write_string(", but the array has invalid negative dimension ", to_standard_error = true);
|
||
write_number(limit, to_standard_error = true);
|
||
}
|
||
|
||
write_string("). Site is ", to_standard_error = true);
|
||
write_string(to_string(filename), to_standard_error = true);
|
||
write_string(":", to_standard_error = true);
|
||
write_number(line_number, to_standard_error = true);
|
||
write_string(".\n", to_standard_error = true);
|
||
|
||
report_bounds_check_error_more_visibly(index, limit, line_number, filename);
|
||
|
||
my_panic();
|
||
}
|
||
|
||
__cast_bounds_check_fail :: (pre_value: s64, pre_flags: u32, post_value: s64, post_flags: u32, fatal: u8, line_number: s64, filename: *u8) #no_context {
|
||
bits : s32 = 0;
|
||
|
||
NUMBER_FLAG_SIGNED :: 0x40;
|
||
NUMBER_FLAG_8BIT :: 0x100;
|
||
NUMBER_FLAG_16BIT :: 0x200;
|
||
NUMBER_FLAG_32BIT :: 0x400;
|
||
NUMBER_FLAG_64BIT :: 0x800;
|
||
|
||
if post_flags & NUMBER_FLAG_64BIT bits = 64;
|
||
if post_flags & NUMBER_FLAG_32BIT bits = 32;
|
||
if post_flags & NUMBER_FLAG_16BIT bits = 16;
|
||
if post_flags & NUMBER_FLAG_8BIT bits = 8;
|
||
|
||
write_string("Cast bounds check failed. Number must be in [", to_standard_error = true);
|
||
|
||
if post_flags & NUMBER_FLAG_SIGNED {
|
||
high_value := (1 << (bits - 1)) - 1;
|
||
low_value := ~high_value;
|
||
write_number(low_value, to_standard_error = true);
|
||
write_string(", ", to_standard_error = true);
|
||
write_number(high_value, to_standard_error = true);
|
||
} else {
|
||
// We don't yet have our stringent definition of left-shift, so, we are generating results
|
||
// that will vary on different machines unless we check. So let's check.
|
||
high_value: u64;
|
||
if bits == 64 {
|
||
high_value = 0xffff_ffff_ffff_ffff;
|
||
} else {
|
||
high_value = ((cast(u64)1) << bits) - 1;
|
||
}
|
||
|
||
write_string("0, ", to_standard_error = true);
|
||
write_nonnegative_number(high_value, to_standard_error = true);
|
||
}
|
||
|
||
write_string("]; it was ", to_standard_error = true);
|
||
|
||
if pre_flags & NUMBER_FLAG_SIGNED {
|
||
write_number(pre_value, to_standard_error = true);
|
||
} else {
|
||
write_nonnegative_number(cast,no_check(u64) pre_value, to_standard_error = true);
|
||
}
|
||
|
||
write_string(". Site is ", to_standard_error = true);
|
||
|
||
write_string(to_string(filename), to_standard_error = true);
|
||
write_string(":", to_standard_error = true);
|
||
write_number(line_number, to_standard_error = true);
|
||
write_string(".\n", to_standard_error = true);
|
||
|
||
if fatal my_panic();
|
||
}
|
||
|
||
__null_pointer_check_fail :: (index: s64, line_number: s64, filename: *u8) #no_context {
|
||
// @Volatile: It is a good idea for these to match the error reports in constant-expression evaluation inside the compiler.
|
||
if index {
|
||
// This is a procedure argument.
|
||
write_string("Null pointer check failed: Argument ", to_standard_error = true);
|
||
write_number(index, to_standard_error = true);
|
||
write_string(" is undergoing an automatic dereference, but the pointer is null. Site is ", to_standard_error = true);
|
||
} else {
|
||
// It was a dereference that happened in some other way.
|
||
write_string("Null pointer check failed: A pointer is undergoing an automatic dereference, but the pointer is null. Site is ", to_standard_error = true);
|
||
}
|
||
|
||
write_string(to_string(filename), to_standard_error = true);
|
||
write_string(":", to_standard_error = true);
|
||
write_number(line_number, to_standard_error = true);
|
||
write_string(".\n", to_standard_error = true);
|
||
|
||
report_null_pointer_error_more_visibly(index, line_number, filename);
|
||
|
||
my_panic();
|
||
}
|
||
|
||
__arithmetic_overflow :: (left: s64, right: s64, type_code: u16, line_number: s64, filename: *u8) #no_context #no_aoc {
|
||
// We have some free bits in type_code...!
|
||
fatal := (type_code & 0x8000);
|
||
signed := (type_code & 0x4000);
|
||
operator_index := (type_code >> 7) & 0x3;
|
||
size := (cast(u64)(type_code & 0x000f))*8;
|
||
|
||
signed_string := ifx signed then "s" else "u";
|
||
operator_string := " / ";
|
||
|
||
if operator_index == 1 then operator_string = " + ";
|
||
else if operator_index == 2 then operator_string = " - ";
|
||
else if operator_index == 3 then operator_string = " * ";
|
||
|
||
write_string("Arithmetic overflow. We tried to compute:\n ", to_standard_error = true);
|
||
|
||
if signed write_number(left, to_standard_error = true);
|
||
else write_nonnegative_number(cast,no_check(u64)left, to_standard_error = true);
|
||
|
||
write_string(operator_string, to_standard_error = true);
|
||
|
||
if signed write_number(right, to_standard_error = true);
|
||
else write_nonnegative_number(cast,no_check(u64)right, to_standard_error = true);
|
||
|
||
write_strings("\nThe operand type is ", signed_string, to_standard_error = true);
|
||
write_nonnegative_number(size, to_standard_error = true);
|
||
write_string(", but the result does not fit into this type.\n", to_standard_error = true);
|
||
|
||
if fatal {
|
||
report_arithmetic_overflow_more_visibly(line_number, filename);
|
||
my_panic();
|
||
}
|
||
}
|
||
|
||
write_string_unsynchronized :: (s: string, to_standard_error := false) #no_context {
|
||
#if OS == .WINDOWS {
|
||
// @Robustness @Incomplete: Fails if s.count doesn't fit in an u32.
|
||
written: u32;
|
||
handle := ifx to_standard_error then windows_standard_error else windows_standard_output;
|
||
status := WriteFile(handle, s.data, cast(u32) s.count, *written, null);
|
||
} else #if OS == .ANDROID {
|
||
// write() compiles and runs fine on Android, but stdout and stderr are piped to /dev/null.
|
||
// So we need to use __android_log_* if we want these strings to appear anywhere.
|
||
// Since __android_log_* functions are line-based, we have to buffer strings until a newline occurs.
|
||
// This has the drawback that the writes without a newline are not visible unless they are followed by a newline, eventually.
|
||
// But timer-based flushing is too complicated for Runtime_Support. :AndroidWriteString
|
||
// -rluba, 2024-01-25
|
||
|
||
if to_standard_error != android_write_string_buffer_is_error {
|
||
// Flush remaining data before switching between stdout and stderr
|
||
android_flush_write_string_buffer();
|
||
}
|
||
android_write_string_buffer_is_error = to_standard_error;
|
||
|
||
for i: 0..s.count-1 {
|
||
c := s[i];
|
||
if c == #char "\n" {
|
||
android_flush_write_string_buffer(force = true);
|
||
} else {
|
||
android_write_string_buffer[android_write_string_buffer_offset] = c;
|
||
android_write_string_buffer_offset += 1;
|
||
if android_write_string_buffer_offset == android_write_string_buffer.count - 1 {
|
||
// Buffer is full. We have to flush, even if there’s no newline
|
||
android_flush_write_string_buffer();
|
||
}
|
||
}
|
||
}
|
||
} else #if OS_IS_UNIX {
|
||
STDOUT_FILENO : s32 : 1;
|
||
STDERR_FILENO : s32 : 2;
|
||
handle := ifx to_standard_error then STDERR_FILENO else STDOUT_FILENO;
|
||
written: s64;
|
||
|
||
while written != s.count {
|
||
result := write(handle, s.data + written, cast(u64) (s.count - written));
|
||
// We should probably be checking errno for failures,
|
||
// but, on macOS at least, we seem to be getting interrupted
|
||
// only be EAGAIN.
|
||
if result < 0 continue;
|
||
|
||
written += result;
|
||
}
|
||
} else #if OS == .WASM {
|
||
wasm_write_string :: (count: s64, data: *void, to_standard_error: bool) #foreign; // You will need to provide this function as JS code in your WASM environment. :JaiWasm:
|
||
wasm_write_string(s.count, s.data, to_standard_error);
|
||
} else {
|
||
// Let the user fill this in, if desired.
|
||
}
|
||
}
|
||
|
||
write_string :: (s: string, to_standard_error := false) #no_context #compiler {
|
||
// write_string is marked #compiler because, if called at compile-time, it
|
||
// involves a different implementation that also syncs with the compiler's output.
|
||
// This runtime version of write_string is just about syncing the threads
|
||
// in your own program.
|
||
|
||
one_time_init(*synch_initted, init_synchronization());
|
||
|
||
runtime_support_acquire(*write_string_lock);
|
||
write_string_unsynchronized(s, to_standard_error);
|
||
runtime_support_release(*write_string_lock);
|
||
}
|
||
|
||
write_strings :: (strings: ..string, to_standard_error := false) #no_context #compiler {
|
||
// write_strings is marked #compiler because, if called at compile-time, it
|
||
// involves a different implementation that also syncs with the compiler's output.
|
||
// This runtime version of write_strings is just about syncing the threads
|
||
// in your own program.
|
||
|
||
one_time_init(*synch_initted, init_synchronization());
|
||
|
||
runtime_support_acquire(*write_string_lock);
|
||
for strings write_string_unsynchronized(it, to_standard_error);
|
||
runtime_support_release(*write_string_lock);
|
||
}
|
||
|
||
#scope_module;
|
||
|
||
write_loc :: (loc: Source_Code_Location, to_standard_error := false) #no_context {
|
||
write_strings(loc.fully_pathed_filename, ":", to_standard_error = to_standard_error);
|
||
write_number(loc.line_number, to_standard_error = to_standard_error);
|
||
write_string(",", to_standard_error);
|
||
write_number(loc.character_number, to_standard_error = to_standard_error);
|
||
}
|
||
|
||
my_panic :: () #no_context {
|
||
write_string("Panic.\n", to_standard_error = true);
|
||
debug_break();
|
||
}
|
||
|
||
__panic_due_to_runtime_call_of_compile_time_procedure :: (line_number: s64, filename: *u8) #no_context {
|
||
write_string("Error: Attempt to call a compile-time procedure at runtime. The location of the call was: ", to_standard_error = true);
|
||
write_string(to_string(filename), to_standard_error = true);
|
||
write_string(":", to_standard_error = true);
|
||
write_number(line_number, to_standard_error = true);
|
||
write_string(".\n", to_standard_error = true);
|
||
|
||
my_panic();
|
||
}
|
||
|
||
#if DEFINE_INITIALIZATION {
|
||
// TEMPORARY_STORAGE_SIZE is defined by the compiler based on Build_Options.
|
||
|
||
first_thread_context: #Context;
|
||
first_thread_temporary_storage: Temporary_Storage;
|
||
first_thread_temporary_storage_data: [TEMPORARY_STORAGE_SIZE] u8 #align 64;
|
||
|
||
#scope_export
|
||
#program_export
|
||
__jai_runtime_init :: (argc: s32, argv: **u8) -> *#Context #c_call {
|
||
__command_line_arguments.count = argc;
|
||
__command_line_arguments.data = argv;
|
||
|
||
// We don't need this initializer_of call, but we're leaving it in a comment, in case there are
|
||
// compiler bugs in Context init and you want to work around them by re-enabling it.
|
||
// @Temporary: Putting it back for a bit since there is a bug exhibiting on Mac that might
|
||
// also cause first_thread_context not to be fully filled out...
|
||
initializer_of(#Context)(*first_thread_context);
|
||
|
||
ts := *first_thread_temporary_storage;
|
||
|
||
ts.data = first_thread_temporary_storage_data.data;
|
||
ts.size = first_thread_temporary_storage_data.count;
|
||
|
||
first_thread_context.temporary_storage = ts;
|
||
|
||
return *first_thread_context;
|
||
}
|
||
|
||
#program_export
|
||
__jai_runtime_fini :: (_context: *void) #c_call {
|
||
// Nothing here for now!
|
||
// We don't bother shutting down the default allocator. The OS frees the memory.
|
||
}
|
||
}
|
||
|
||
__instrumentation_first :: () {
|
||
// These are places to insert code, from a metaprogram, if you want
|
||
// that code to run before main().
|
||
}
|
||
|
||
__instrumentation_second :: () {
|
||
}
|
||
|
||
#scope_module
|
||
|
||
#if DEFINE_SYSTEM_ENTRY_POINT {
|
||
#program_export "main"
|
||
__system_entry_point :: (argc: s32, argv: **u8) -> s32 #c_call {
|
||
__jai_runtime_init(argc, argv);
|
||
|
||
push_context first_thread_context {
|
||
#if ENABLE_BACKTRACE_ON_CRASH {
|
||
// It's possible we should move the init of the crash handler to after __instrumentation_*, so that
|
||
// plugins can set it? But, plugin inits can crash. So maybe we need some other way for a plugin
|
||
// to say it's taking over the crash handler.
|
||
|
||
Handler :: #import "Runtime_Support_Crash_Handler";
|
||
Handler.init();
|
||
}
|
||
|
||
__instrumentation_first ();
|
||
__instrumentation_second();
|
||
|
||
__program_main :: () #entry_point;
|
||
no_inline __program_main();
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
Default_Allocator :: #import "Default_Allocator";
|
||
|
||
runtime_support_default_allocator_proc :: Default_Allocator.allocator_proc;
|
||
|
||
#scope_export
|
||
|
||
compile_time_debug_break :: () #compiler #no_context;
|
||
|
||
debug_break :: () #no_context {
|
||
if #compile_time {
|
||
compile_time_debug_break();
|
||
} else {
|
||
// Break into the debugger, or stop the running process.
|
||
#if OS == .PS5 {
|
||
#asm { int 0x41; }
|
||
} else #if CPU == .X64 {
|
||
#asm { int3; }
|
||
} else #if CPU == .ARM64 {
|
||
#bytes .[0x20, 0x00, 0b001_0_0000, 0b1101_0100]; // BRK 0x01: 11 bit instruction, 16 bit immediate, 5 bit unused.
|
||
} else #if OS == .WASM {
|
||
wasm_debug_break :: () #foreign; // You will need to provide this function as JS code in your WASM environment. :JaiWasm:
|
||
wasm_debug_break();
|
||
} else {
|
||
llvm_trap :: () #intrinsic "llvm.debugtrap"; // @ToDo: This used to be llvm.trap which supposedly doesn’t work on Android. We need to test if "debugtrap" works there or if we need a different implemenation.
|
||
llvm_trap();
|
||
}
|
||
}
|
||
}
|
||
|
||
// one_time_init is exposed because maybe user libraries want to use a similar thing.
|
||
one_time_init :: (synch_value: *s32, to_insert: Code) #expand {
|
||
// A routine to run the code in 'to_insert' only once, even if
|
||
// there are multiple threads. synch_value.* should be 0 at startup.
|
||
|
||
// The values of synch_value.*:
|
||
// 0 = uninitialized, 1 = in progress init, 2 = initialized.
|
||
|
||
// Courtesy of Jeff Roberts.
|
||
|
||
while 1 {
|
||
// @Warning: Apparently this read may not work on ARM, we need to look into it.
|
||
// The goal here is just to avoid the overhead of spamming the compare_and_swap.
|
||
if synch_value.* == 2 break;
|
||
|
||
success, old := compare_and_swap(synch_value, 0, 1);
|
||
if old == {
|
||
case 0;
|
||
#insert to_insert;
|
||
|
||
if !compare_and_swap(synch_value, 1, 2) debug_break(); // Should not happen!
|
||
case 1;
|
||
// Maybe some exponential fall offy thing here?
|
||
#if CPU == .X64 {
|
||
for 1..4 #asm { pause; pause; pause; pause; pause; }
|
||
}
|
||
#if CPU == .ARM64 {
|
||
#bytes .[0x3F, 0x20, 0x03, 0xD5]; // YIELD
|
||
}
|
||
case 2;
|
||
}
|
||
}
|
||
}
|
||
|
||
// @Volatile: Context_Base must match internal compiler settings in general.h
|
||
// It must exist and be relatively easy to compile (without requiring #run of
|
||
// procedures, for example). Context_Base is going to be annoying to edit because
|
||
// if you make an error of some kind, even a simple one like an undeclared identifier,
|
||
// you won't get a helpful error message.
|
||
Context_Base :: struct {
|
||
context_info: *Type_Info_Struct; // Allow libs or DLLs to see what context we are passing them. Will always be initialized to type_of(Context).
|
||
|
||
thread_index : u32;
|
||
|
||
allocator := default_allocator;
|
||
|
||
logger := runtime_support_default_logger;
|
||
logger_data : *void;
|
||
log_source_identifier: u64; // Arbitrary identifier; settable by the main program.
|
||
log_level : Log_Level; // Settable by the main program to inform anyone who logs.
|
||
|
||
temporary_storage: *Temporary_Storage;
|
||
|
||
// Currently, for simplicity we don't #if this out right now when _STACK_TRACE is false
|
||
// (but may later). It is probably convenient to let runtime code be able
|
||
// to check context.stack_trace to see if it is null in some cases,
|
||
// rather than needing all that to be #iffed as well. We will see.
|
||
stack_trace: *Stack_Trace_Node;
|
||
|
||
assertion_failed := runtime_support_assertion_failed;
|
||
handling_assertion_failure := false; // Used to avoid assert() infinite loops. Do not mess with this value.
|
||
|
||
default_allocator :: Allocator.{runtime_support_default_allocator_proc, null};
|
||
}
|
||
|
||
Temporary_Storage :: struct { // @Volatile: Must match general.h
|
||
data: *u8;
|
||
size: s64;
|
||
|
||
total_bytes_occupied: s64;
|
||
high_water_mark: s64;
|
||
last_set_mark_location: Source_Code_Location;
|
||
|
||
implementation_data: [256] u8;
|
||
}
|
||
|
||
#scope_module
|
||
|
||
init_synchronization :: () #no_context {
|
||
// For now, we do not bother to destroy write_string_lock, etc
|
||
// on shutdown.
|
||
#if OS == .WINDOWS {
|
||
InitializeCriticalSection :: (section: *CRITICAL_SECTION_stub) #foreign kernel32;
|
||
InitializeCriticalSection(*write_string_lock);
|
||
|
||
STD_OUTPUT_HANDLE :: -11;
|
||
STD_ERROR_HANDLE :: -12;
|
||
windows_standard_output = GetStdHandle(STD_OUTPUT_HANDLE);
|
||
windows_standard_error = GetStdHandle(STD_ERROR_HANDLE);
|
||
} else #if OS == .LINUX || OS == .ANDROID {
|
||
// Nothing to do
|
||
} else #if OS_IS_UNIX { // Other Unixes
|
||
PTHREAD_MUTEX_RECURSIVE :: 2;
|
||
#if OS == .MACOS || OS == .IOS {
|
||
pthread_mutexattr_t_stub :: struct { memory: [2] s64; }
|
||
} else {
|
||
pthread_mutexattr_t_stub :: *void;
|
||
}
|
||
pthread_mutexattr_init :: (attr: *pthread_mutexattr_t_stub) -> s32 #foreign pthreads;
|
||
pthread_mutexattr_settype :: (attr: *pthread_mutexattr_t_stub, type: s32) -> s32 #foreign pthreads;
|
||
pthread_mutex_init :: (mutex: *pthread_mutex_t_stub, attr: *pthread_mutexattr_t_stub) -> s32 #foreign pthreads;
|
||
|
||
attr: pthread_mutexattr_t_stub;
|
||
pthread_mutexattr_init(*attr);
|
||
pthread_mutexattr_settype(*attr, PTHREAD_MUTEX_RECURSIVE);
|
||
pthread_mutex_init(*write_string_lock, *attr); // This had better not fail!
|
||
} else {
|
||
// Let the user fill this in, if desired.
|
||
}
|
||
}
|
||
|
||
#if OS == .WINDOWS {
|
||
kernel32 :: #library,system "kernel32";
|
||
|
||
WriteFile :: (handle: *void, buffer: *void, buffer_length: u32, written_result: *u32, overlapped: *void) -> s32 #foreign kernel32;
|
||
GetStdHandle :: (handle: s32) -> *void #foreign kernel32;
|
||
|
||
EnterCriticalSection :: (lpCriticalSection: *CRITICAL_SECTION_stub) #foreign kernel32;
|
||
LeaveCriticalSection :: (lpCriticalSection: *CRITICAL_SECTION_stub) #foreign kernel32;
|
||
|
||
runtime_support_acquire :: EnterCriticalSection;
|
||
runtime_support_release :: LeaveCriticalSection;
|
||
write_string_lock: CRITICAL_SECTION_stub;
|
||
|
||
// Our mutex / critical sections are just dummies to hold as much memory as we need, so we don't bloat Runtime_Support.
|
||
CRITICAL_SECTION_stub :: struct {
|
||
memory: [5] u64;
|
||
}
|
||
|
||
windows_standard_output: *void;
|
||
windows_standard_error: *void;
|
||
} else #if OS_IS_UNIX {
|
||
libc :: #library,system "libc";
|
||
write :: (fd: s32, buf: *void, count: u64) -> s64 #foreign libc;
|
||
|
||
#if OS == .LINUX || OS == .ANDROID {
|
||
// Use futex to avoid depending on pthread
|
||
write_string_lock: s32; // Zero if unlocked.
|
||
|
||
FUTEX_WAIT :: 0;
|
||
FUTEX_WAKE :: 1;
|
||
FUTEX_PRIVATE_FLAG :: 128;
|
||
|
||
runtime_support_acquire :: (futex_lock: *s32) #no_context {
|
||
SPIN_BUDGET :: 10;
|
||
|
||
loop_count := 0;
|
||
|
||
// Retry to acquire the lock until we got it.
|
||
while true {
|
||
if compare_and_swap(futex_lock, 0, 1) {
|
||
// The mutex was free and is now acquired.
|
||
return;
|
||
}
|
||
|
||
// Spin wait before trying to acquire the mutex again.
|
||
if loop_count < SPIN_BUDGET {
|
||
loop_count += 1;
|
||
|
||
for 0..(1 << loop_count) {
|
||
#if CPU == .X64 {
|
||
#asm { pause; pause; pause; pause; }
|
||
}
|
||
#if CPU == .ARM64 {
|
||
#bytes .[0x3F, 0x20, 0x03, 0xD5]; // YIELD
|
||
}
|
||
}
|
||
} else {
|
||
// Spinning takes too long, let's block and try re-acquire after wake up.
|
||
futex(futex_lock, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 1);
|
||
loop_count = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
runtime_support_release :: (futex_lock: *s32) #no_context {
|
||
compare_and_swap(futex_lock, 1, 0); // lock_xchg would be enough, but we don’t have that intrinsic
|
||
futex(futex_lock, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1);
|
||
}
|
||
|
||
futex :: (futex_lock: *s32, op: u64, val: u64) #expand {
|
||
#if CPU == .X64 {
|
||
SYS_futex :: 202;
|
||
#asm SYSCALL_SYSRET {
|
||
mov.q call: gpr === a, SYS_futex;
|
||
mov.q uaddr: gpr === di, futex_lock;
|
||
mov.q futex_op: gpr === si, op;
|
||
mov.q value: gpr === d, val;
|
||
mov.q timeout: gpr === 10, 0;
|
||
|
||
syscall t1:, t2:, call;
|
||
}
|
||
} else {
|
||
SYS_futex :: 98;
|
||
syscall :: (__sysno: s64, __args: ..Any) -> s64 #foreign libc;
|
||
syscall(SYS_futex, futex_lock, op, val, null, null, 0);
|
||
}
|
||
}
|
||
} else {
|
||
#if OS == .MACOS || OS == .IOS {
|
||
pthreads :: #library,system "libc";
|
||
} else {
|
||
pthreads :: #library,system "libpthread";
|
||
}
|
||
|
||
pthread_mutex_lock :: (mutex: *pthread_mutex_t_stub) -> s32 #foreign pthreads;
|
||
pthread_mutex_unlock :: (mutex: *pthread_mutex_t_stub) -> s32 #foreign pthreads;
|
||
|
||
runtime_support_acquire :: pthread_mutex_lock;
|
||
runtime_support_release :: pthread_mutex_unlock;
|
||
write_string_lock: pthread_mutex_t_stub;
|
||
|
||
// Our mutex / critical sections are just dummies to hold as much memory as we need, so we don't bloat Runtime_Support.
|
||
// @Volatile: These types have to fit the real declarations on each platform!
|
||
#if OS == .MACOS || OS == .IOS {
|
||
pthread_mutex_t_stub :: struct {
|
||
memory: [64] u8;
|
||
}
|
||
} else {
|
||
pthread_mutex_t_stub :: *void;
|
||
}
|
||
}
|
||
} else {
|
||
// Let the user fill this in, if desired.
|
||
write_string_lock: void;
|
||
runtime_support_acquire :: (lock: *$T) #no_context #expand {}
|
||
runtime_support_release :: (lock: *$T) #no_context #expand {}
|
||
}
|
||
|
||
#if OS == .ANDROID {
|
||
android_write_string_buffer_offset := 0;
|
||
android_write_string_buffer_is_error: bool;
|
||
android_write_string_buffer: [512] u8; // Android offers no bare stdio functions, only loggers – which append "\n". So we have to buffer partial lines… :AndroidWriteString
|
||
#scope_export
|
||
android_flush_write_string_buffer :: (force := false) #no_context {
|
||
if !force && !android_write_string_buffer_offset return;
|
||
|
||
liblog :: #library,system "liblog";
|
||
ANDROID_LOG_INFO: s32 : 4;
|
||
ANDROID_LOG_ERROR: s32 : 6;
|
||
__android_log_write :: (prio: s32, tag: *u8, fmt: *u8, args: .. Any) -> s32 #foreign liblog;
|
||
|
||
level := ifx android_write_string_buffer_is_error then ANDROID_LOG_ERROR else ANDROID_LOG_INFO;
|
||
tag := ifx android_write_string_buffer_is_error then "jai-stderr".data else "jai-stdout".data;
|
||
|
||
android_write_string_buffer[android_write_string_buffer_offset] = 0;
|
||
__android_log_write(level, tag, android_write_string_buffer.data);
|
||
android_write_string_buffer_offset = 0;
|
||
}
|
||
}
|
||
|
||
synch_initted: s32 = 0;
|
||
|
||
|
||
|
||
// ==========================================================================================================================================
|
||
// Below are the functions we've added.
|
||
|
||
#scope_file
|
||
|
||
|
||
report_bounds_check_error_more_visibly :: (index: s64, limit: s64, line_number: s64, filename: *u8) #no_context {
|
||
report_error_in_yer_face_no_ctx("Array bounds check failed in file % at line number %: \nProvided index: %, maximum index: %", to_string(filename), line_number, index, limit-1);
|
||
}
|
||
|
||
report_null_pointer_error_more_visibly :: (index: s64, line_number: s64, filename: *u8) #no_context {
|
||
if index {
|
||
// This is a procedure argument.
|
||
report_error_in_yer_face_no_ctx("Null pointer check failed in file % at line number %: \nArgument % is undergoing an automatic dereference, but the pointer is null.", to_string(filename), line_number, index);
|
||
} else {
|
||
// It was a dereference that happened in some other way.
|
||
report_error_in_yer_face_no_ctx("Null pointer check failed in file % at line number %: \nA pointer is undergoing an automatic dereference, but the pointer is null.", to_string(filename), line_number);
|
||
}
|
||
}
|
||
|
||
report_arithmetic_overflow_more_visibly :: (line_number: s64, filename: *u8) #no_context {
|
||
report_error_in_yer_face_no_ctx("Arithmetic overflow in file % at line number %: \nCheck the logs for more information.", to_string(filename), line_number);
|
||
}
|
||
|
||
report_failed_assertion_more_visibly :: (loc: Source_Code_Location, msg: string) {
|
||
report_error_in_yer_face("Assertion failed in file % at line %, char %: \n%", loc.fully_pathed_filename, loc.line_number, loc.character_number, msg);
|
||
}
|
||
|
||
#scope_export
|
||
// Set by the program: logs the crash report to the session log and waits for it to be written.
|
||
crash_log_hook: #type (crash_report: string);
|
||
// Set by the program: the crash report is <folder>-crash-report.txt, beside the executable on
|
||
// Windows and in the per-user data folder of this name on Linux and macOS.
|
||
crash_report_folder_name := "app";
|
||
#scope_file
|
||
|
||
report_error_in_yer_face_no_ctx :: (message: string, args: .. Any) #no_context {
|
||
ctx: #Context;
|
||
push_context ctx { report_error_in_yer_face(message, ..args, from_no_context = true); }
|
||
}
|
||
|
||
report_error_in_yer_face :: (message: string, args: .. Any, from_no_context := false) {
|
||
using _ :: #import "System"; // 'using' ensures the imports stay within this scope. (Raw imports do not respect scopes)
|
||
using _ :: #import "String";
|
||
using _ :: #import "Basic";
|
||
using _ :: #import "File";
|
||
|
||
formatted_message := tprint(message, ..args);
|
||
|
||
builder: String_Builder;
|
||
append(*builder, formatted_message);
|
||
append(*builder, "\n\n");
|
||
if from_no_context || !context.stack_trace {
|
||
print_to_builder(*builder, "Stack trace not available%.\n", ifx from_no_context " (it was lost due to a #no_context call)");
|
||
} else {
|
||
append(*builder, "Stack trace:\n");
|
||
node := context.stack_trace;
|
||
while node {
|
||
if node.info print_to_builder(*builder, "%:%: %\n", node.info.location.fully_pathed_filename, node.line_number, node.info.name);
|
||
node = node.next;
|
||
}
|
||
}
|
||
crash_report := builder_to_string(*builder);
|
||
|
||
// Create a crash report file
|
||
data_dir := path_strip_filename(get_path_of_running_executable());
|
||
|
||
#if OS == .LINUX {
|
||
XDG :: #import "XDG";
|
||
data_dir = sprint("%/%", XDG.get_path(.DATA_HOME), crash_report_folder_name);
|
||
}
|
||
#if OS == .MACOS {
|
||
using _ :: #import "Objective_C";
|
||
application_support := to_string(NSURL.path(NSFileManager.URLForDirectory(NSFileManager.defaultManager(), NSApplicationSupportDirectory, NSUserDomainMask, null, YES, null)));
|
||
data_dir = sprint("%/%", application_support, crash_report_folder_name);
|
||
}
|
||
|
||
data_dir = trim_right(data_dir, "/");
|
||
make_directory_if_it_does_not_exist(data_dir);
|
||
crash_report_file_path := tprint("%/%-crash-report.txt", data_dir, crash_report_folder_name);
|
||
write_entire_file(crash_report_file_path, crash_report);
|
||
|
||
// The crash report is overwritten every new crash, so we also print it to the session log for safekeeping.
|
||
// The program's hook queues it and waits for the log to reach the disk; #no_context strips the
|
||
// logger from context, so without the hook a #no_context crash can't reach the session log.
|
||
if crash_log_hook {
|
||
crash_log_hook(crash_report);
|
||
} else if !from_no_context {
|
||
log_error(crash_report, flags = .TO_FILE_ONLY);
|
||
}
|
||
|
||
// Show a Windows message box
|
||
#if OS == .WINDOWS {
|
||
W :: #import "Windows";
|
||
W_Utf8 :: #import "Windows_Utf8";
|
||
W.ShowCursor(1);
|
||
W.MessageBoxW(null, W_Utf8.utf8_to_wide(formatted_message,, allocator = temp), W_Utf8.utf8_to_wide("Fatal Error",, allocator = temp), W.MB_OK);
|
||
}
|
||
}
|
||
|
||
// END of the functions we've added
|
||
// ==========================================================================================================================================
|