From 88dee75f7d6b06835d469437989cc5a089eb2302 Mon Sep 17 00:00:00 2001 From: Synthasmagoria Date: Thu, 30 Jul 2026 20:16:09 +0200 Subject: [PATCH] fuck submodules part 3 --- libraries/back/LICENSE | 19 ++ libraries/back/README.md | 188 +++++++++++++ libraries/back/allocator.odin | 230 +++++++++++++++ libraries/back/back.odin | 147 ++++++++++ libraries/back/back_darwin.odin | 160 +++++++++++ libraries/back/back_linux.odin | 169 +++++++++++ libraries/back/back_other.odin | 127 +++++++++ libraries/back/back_windows.odin | 140 +++++++++ libraries/back/signal_handler_other.odin | 11 + libraries/back/signal_handler_posix.odin | 37 +++ libraries/back/signal_handler_windows.odin | 38 +++ libraries/snths_ui/example/back/LICENSE | 19 ++ libraries/snths_ui/example/back/README.md | 188 +++++++++++++ .../snths_ui/example/back/allocator.odin | 230 +++++++++++++++ libraries/snths_ui/example/back/back.odin | 147 ++++++++++ .../snths_ui/example/back/back_darwin.odin | 160 +++++++++++ .../snths_ui/example/back/back_linux.odin | 169 +++++++++++ .../snths_ui/example/back/back_other.odin | 127 +++++++++ .../snths_ui/example/back/back_windows.odin | 140 +++++++++ .../example/back/signal_handler_other.odin | 11 + .../example/back/signal_handler_posix.odin | 37 +++ .../example/back/signal_handler_windows.odin | 38 +++ libraries/snths_ui/example/main.odin | 116 ++++++++ libraries/snths_ui/example/main_linux.odin | 38 +++ libraries/snths_ui/example/main_other.odin | 11 + libraries/snths_ui/ui.odin | 265 ++++++++++++++++++ 26 files changed, 2962 insertions(+) create mode 100644 libraries/back/LICENSE create mode 100644 libraries/back/README.md create mode 100644 libraries/back/allocator.odin create mode 100644 libraries/back/back.odin create mode 100644 libraries/back/back_darwin.odin create mode 100644 libraries/back/back_linux.odin create mode 100644 libraries/back/back_other.odin create mode 100644 libraries/back/back_windows.odin create mode 100644 libraries/back/signal_handler_other.odin create mode 100644 libraries/back/signal_handler_posix.odin create mode 100644 libraries/back/signal_handler_windows.odin create mode 100644 libraries/snths_ui/example/back/LICENSE create mode 100644 libraries/snths_ui/example/back/README.md create mode 100644 libraries/snths_ui/example/back/allocator.odin create mode 100644 libraries/snths_ui/example/back/back.odin create mode 100644 libraries/snths_ui/example/back/back_darwin.odin create mode 100644 libraries/snths_ui/example/back/back_linux.odin create mode 100644 libraries/snths_ui/example/back/back_other.odin create mode 100644 libraries/snths_ui/example/back/back_windows.odin create mode 100644 libraries/snths_ui/example/back/signal_handler_other.odin create mode 100644 libraries/snths_ui/example/back/signal_handler_posix.odin create mode 100644 libraries/snths_ui/example/back/signal_handler_windows.odin create mode 100644 libraries/snths_ui/example/main.odin create mode 100644 libraries/snths_ui/example/main_linux.odin create mode 100644 libraries/snths_ui/example/main_other.odin create mode 100644 libraries/snths_ui/ui.odin diff --git a/libraries/back/LICENSE b/libraries/back/LICENSE new file mode 100644 index 0000000..d97adb1 --- /dev/null +++ b/libraries/back/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Laytan Laats + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libraries/back/README.md b/libraries/back/README.md new file mode 100644 index 0000000..50f25bf --- /dev/null +++ b/libraries/back/README.md @@ -0,0 +1,188 @@ +# Back + +Backtraces for Odin, see examples below and in the examples folder. + +To change the size (amount of stackframes to print) in places where this can't be set directly, you can use the `-define:BACKTRACE_SIZE=16`. + +## Targets + +### Windows, MacoOS & Linux + +These targets use debug information that's added by compiling with `-debug` to provide traces and give rough information when compiled without it. + +Performance is not impacted at all during normal running, just when you ask for a trace or lines. + +Per platform notes below: + +#### Windows + +For Windows support, all credit goes to [DaseinPhaos/pdb](https://github.com/DaseinPhaos/pdb). + +Windows is not able to get *any* information when not compiled with `-debug`. + +NOTE: The pdb package allocates a lot of stuff and does not really provide a way of deleting the allocations, so, before calling into the package, this package sets it to use the `context.temporary_allocator`. + +#### MacOS + +Uses a private framework for symbolication, and thus will not get through Apple's review process. + +#### Linux + +On Linux, the `addr2line` command is invoked, which comes pre-installed (maybe in binutils). + +The `addr2line` command can be changed by setting the `-define:BACK_ADDR2LINE_PATH=your/atos` flag. + +The path to the running binary is also needed for this command, this is `os.args[0]` by default and to my knowledge is always correct. +Nevertheless it can be changed with the `-define:BACK_PROGRAM=path/to/binary` flag. + +I am planning on rewriting this implementation to not require an external command like this by parsing the DWARF debug information manually. + +### Others + +Other targets use the instrumentation features of Odin to keep track of stack frames, this has a minimal impact on performance and size. + +Forcing the instrumentation based implementation on Windows, Linux and Darwin can be done with `-define:BACK_FORCE_FALLBACK=true`. + +NOTE: this implementation requires at least `-o:minimal` as it requires `#force_inline` procs to actually be inlined, this is not the case with `-o:none`. + +NOTE: the instrumentation features and WASM combination is a bit fragile and seems to only work on `-o:minimal` exclusively, this is almost certainly a codegen bug. + +## Manual + +```odin +package manual + +import "core:fmt" + +import back "../.." + +main :: proc() { + // Allocates for 16 frames. + bt := back.trace_n(16) + print(bt) + + // Or, doesn't allocate, returns `-define:BACKTRACE_SIZE` frames. + btc := back.trace() + print(btc.trace[:btc.len]) + + // Or, fill in a slice. + bt = make(back.Trace, 16) + bt = bt[:back.trace_fill(bt)] + print(bt) +} + +print :: proc(bt: back.Trace) { + lines, err := back.lines(bt) + if err != nil { + fmt.eprintf("Could not retrieve backtrace lines: %v\n", err) + } else { + defer back.lines_destroy(lines) + + fmt.eprintln("[back trace]") + back.print(lines) + } +} + +// [back trace] +// back.trace_n - /Users/laytan/projects/back/back.odin:59 +// manual.main - /Users/laytan/projects/back/examples/manual/main.odin:9 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// manual.main - /Users/laytan/projects/back/examples/manual/main.odin:13 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +// [back trace] +// back.trace_fill - /Users/laytan/projects/back/back.odin:64 +// manual.main - /Users/laytan/projects/back/examples/manual/main.odin:18 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +``` + +## Tracking Allocator + +```odin +package main + +import "back" + +_main :: proc() { + _ = new(int) + free(rawptr(uintptr(100))) +} + +main :: proc() { + track: back.Tracking_Allocator + back.tracking_allocator_init(&track, context.allocator) + defer back.tracking_allocator_destroy(&track) + + context.allocator = back.tracking_allocator(&track) + defer back.tracking_allocator_print_results(&track) + + _main() +} + +// /Users/laytan/projects/back/examples/allocator/main.odin(6:6) leaked 8b +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// back.tracking_allocator_proc - /Users/laytan/projects/back/allocator.odin:129 +// runtime.mem_alloc_bytes - /Users/laytan/third-party/Odin/core/runtime/internal.odin:141 +// runtime.new_aligned-13136 - /Users/laytan/third-party/Odin/core/runtime/core_builtin.odin:250 +// runtime.new-13097 - /Users/laytan/third-party/Odin/core/runtime/core_builtin.odin:246 +// main._main - /Users/laytan/projects/back/examples/allocator/main.odin:6 +// main.main - /Users/laytan/projects/back/examples/allocator/main.odin:18 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +// +// +// /Users/laytan/projects/back/examples/allocator/main.odin(7:2) allocation 64 was freed badly +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// back.tracking_allocator_proc - /Users/laytan/projects/back/allocator.odin:102 +// runtime.mem_free - /Users/laytan/third-party/Odin/core/runtime/internal.odin:162 +// main._main - /Users/laytan/projects/back/examples/allocator/main.odin:8 +// main.main - /Users/laytan/projects/back/examples/allocator/main.odin:18 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +``` + +## Printing a backtrace on assertion failures / panics + +```odin +package main + +import "back" + +main :: proc() { + context.assertion_failure_proc = back.assertion_failure_proc + assert(3 == 2) +} + +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// back.assertion_failure_proc - /Users/laytan/projects/back/back.odin:97 +// runtime.assert.internal-0 - /Users/laytan/third-party/Odin/core/runtime/core_builtin.odin:818 +// runtime.assert - /Users/laytan/third-party/Odin/core/runtime/core_builtin.odin:820 +// main.main - /Users/laytan/projects/back/examples/assert_backtrace/main.odin:8 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +// /Users/laytan/projects/back/examples/assert_backtrace/main.odin(7:5) runtime assertion +``` + +## Printing a backtrace on segmentation faults + +```odin +package main + +import "back" + +main :: proc() { + back.register_segfault_handler() + + ptr: ^int + bad := ptr^ + 2 + _ = bad +} + +// Segmentation Fault +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// back.register_segfault_handler$anon-1 - /Users/laytan/projects/back/back.odin:114 +// ?? - ?? +// main.main - /Users/laytan/projects/back/examples/segfault/main.odin:8 +``` diff --git a/libraries/back/allocator.odin b/libraries/back/allocator.odin new file mode 100644 index 0000000..986e424 --- /dev/null +++ b/libraries/back/allocator.odin @@ -0,0 +1,230 @@ +#+vet explicit-allocators +package back + +import "base:runtime" + +import "core:fmt" +import "core:mem" +import "core:sync" + +// The backtrace tracking allocator is a similar allocator as the `core:mem` tracking allocator but keeps +// backtraces for each allocation. +// +// See examples/allocator for a usage snippet. +// +// Print results at the end using tracking_allocator_print_results(). +Tracking_Allocator :: struct { + backing: mem.Allocator, + internals_allocator: mem.Allocator, + allocation_map: map[rawptr]Tracking_Allocator_Entry, + bad_free_array: [dynamic]Tracking_Allocator_Bad_Free_Entry, + mutex: sync.Mutex, + clear_on_free_all: bool, +} + +Tracking_Allocator_Entry :: struct { + memory: rawptr, + size: int, + alignment: int, + mode: mem.Allocator_Mode, + err: mem.Allocator_Error, + location: runtime.Source_Code_Location, + backtrace: Trace_Const, +} + +Tracking_Allocator_Bad_Free_Entry :: struct { + memory: rawptr, + location: runtime.Source_Code_Location, + backtrace: Trace_Const, +} + +tracking_allocator_init :: proc( + t: ^Tracking_Allocator, + backing_allocator: mem.Allocator, + internals_allocator := context.allocator, +) { + t.backing = backing_allocator + t.internals_allocator = internals_allocator + t.allocation_map.allocator = internals_allocator + t.bad_free_array.allocator = internals_allocator + + if .Free_All in mem.query_features(t.backing) { + t.clear_on_free_all = true + } +} + +tracking_allocator_destroy :: proc(t: ^Tracking_Allocator) { + delete(t.allocation_map) + delete(t.bad_free_array) +} + +tracking_allocator_clear :: proc(t: ^Tracking_Allocator) { + sync.guard(&t.mutex) + + clear(&t.allocation_map) + clear(&t.bad_free_array) +} + +@(require_results) +tracking_allocator :: proc(data: ^Tracking_Allocator) -> mem.Allocator { + return mem.Allocator{data = data, procedure = tracking_allocator_proc} +} + +tracking_allocator_proc :: proc( + allocator_data: rawptr, + mode: mem.Allocator_Mode, + size, alignment: int, + old_memory: rawptr, + old_size: int, + loc := #caller_location, +) -> ( + result: []byte, + err: mem.Allocator_Error, +) { + data := (^Tracking_Allocator)(allocator_data) + + sync.mutex_guard(&data.mutex) + + if mode == .Query_Info { + info := (^mem.Allocator_Query_Info)(old_memory) + if info != nil && info.pointer != nil { + if entry, ok := data.allocation_map[info.pointer]; ok { + info.size = entry.size + info.alignment = entry.alignment + } + info.pointer = nil + } + + return + } + + if mode == .Free && old_memory != nil && old_memory not_in data.allocation_map { + append( + &data.bad_free_array, + Tracking_Allocator_Bad_Free_Entry{ + memory = old_memory, + location = loc, + backtrace = trace(), + }, + ) + } else { + result = data.backing.procedure( + data.backing.data, + mode, + size, + alignment, + old_memory, + old_size, + loc, + ) or_return + } + result_ptr := raw_data(result) + + if data.allocation_map.allocator.procedure == nil { + data.allocation_map.allocator = context.allocator + } + + switch mode { + case .Alloc, .Alloc_Non_Zeroed: + data.allocation_map[result_ptr] = Tracking_Allocator_Entry { + memory = result_ptr, + size = size, + mode = mode, + alignment = alignment, + err = err, + location = loc, + backtrace = trace(), + } + case .Free: + delete_key(&data.allocation_map, old_memory) + case .Free_All: + if data.clear_on_free_all { + clear_map(&data.allocation_map) + } + case .Resize, .Resize_Non_Zeroed: + if old_memory != result_ptr { + delete_key(&data.allocation_map, old_memory) + } + data.allocation_map[result_ptr] = Tracking_Allocator_Entry { + memory = result_ptr, + size = size, + mode = mode, + alignment = alignment, + err = err, + location = loc, + backtrace = trace(), + } + + case .Query_Features: + set := (^mem.Allocator_Mode_Set)(old_memory) + if set != nil { + set^ = { + .Alloc, + .Alloc_Non_Zeroed, + .Free, + .Free_All, + .Resize, + .Query_Features, + .Query_Info, + } + } + return nil, nil + + case .Query_Info: + unreachable() + } + + return +} + +tracking_allocator_print_results :: proc(t: ^Tracking_Allocator, temp_allocator := context.temp_allocator) { + i: int + ALLOCATOR_MAX_BACKTRACES :: 16 + + for _, leak in t.allocation_map { + fmt.eprintfln("\x1b[31m%v leaked %m\x1b[0m", leak.location, leak.size) + + defer i += 1 + if i > ALLOCATOR_MAX_BACKTRACES { + continue + } + + trace, err := lines(leak.backtrace, temp_allocator, temp_allocator) + defer lines_destroy(trace, temp_allocator) + + fmt.eprintln("[back trace]") + + if err != nil { + fmt.eprintfln("backtrace error: %v", err) + continue + } + + print(trace, temp_allocator=temp_allocator) + fmt.eprintln() + } + + for bad_free, _ in t.bad_free_array { + fmt.eprintfln( + "\x1b[31m%v allocation %p was freed badly\x1b[0m", + bad_free.location, + bad_free.memory, + ) + + defer i += 1 + if i > ALLOCATOR_MAX_BACKTRACES { + continue + } + + trace, err := lines(bad_free.backtrace, temp_allocator, temp_allocator) + defer lines_destroy(trace, temp_allocator) + + fmt.eprintln("[back trace]") + + if err != nil { + fmt.eprintf("backtrace error: %v\n", err) + continue + } + + print(trace, temp_allocator=temp_allocator) + } +} diff --git a/libraries/back/back.odin b/libraries/back/back.odin new file mode 100644 index 0000000..80a4136 --- /dev/null +++ b/libraries/back/back.odin @@ -0,0 +1,147 @@ +#+vet explicit-allocators +package back + +import "base:runtime" + +import "core:fmt" +import "core:io" +import "core:os" +import "core:sync" +import "core:text/table" + +// Size of a constant backtrace, as used by the tracking allocator for example. +BACKTRACE_SIZE :: #config(BACKTRACE_SIZE, 16) + +// For targets that do not have native support (using debug info), +// backtraces are done through instrumentation, Odin only allows one enter/exit instrumentation +// procedure though, so you can set this to true, add your own instrumentation procs, and have +// them call `back.other_instrumentation_enter` and `back.other_instrumentation_exit` to hook +// up the backtraces. +// The custom proc must have `#force_inline`. +OTHER_CUSTOM_INSTRUMENTATION :: #config(BACK_OTHER_CUSTOM_INSTRUMENTATION, false) + +// Force the fallback instrumentation based implementation instead of debug info based. +FORCE_FALLBACK :: #config(BACK_FORCE_FALLBACK, false) + +// Fallback requires a single module (subtle bugs with multiple modules and instrumentation in Odin), +// and at least -o:minimal (#force_inline has to actually inline). +_COULD_USE_FALLBACK_WITHOUT_ERROR :: !ODIN_USE_SEPARATE_MODULES && ODIN_OPTIMIZATION_MODE >= .Minimal + +// Use the fallback (instrumentation based) implementation: +// if it is forced, or it can be used without error and debug info is off, or if the target has no debug info based support. +USE_FALLBACK :: FORCE_FALLBACK || (_COULD_USE_FALLBACK_WITHOUT_ERROR && !ODIN_DEBUG) || (ODIN_OS != .Darwin && ODIN_OS != .Linux && ODIN_OS != .Windows) + +ADDR2LINE_PATH :: #config(TRACE_ADDR2LINE_PATH, "addr2line") + +Trace :: []Trace_Entry + +Trace_Const :: struct { + trace: [BACKTRACE_SIZE]Trace_Entry, + len: int, +} + +// Platform specific. +Trace_Entry :: _Trace_Entry + +Line :: struct { + location: string, + symbol: string, +} + +// TODO: improve errors. +Lines_Error :: enum { + None, + Parse_Address_Fail, + Addr2line_Unexpected_EOF, + Addr2line_Output_Error, + Addr2line_Unresolved, + Addr2line_Process_Error, + Out_Of_Memory, + Info_Not_Found, +} + +// TODO: arbitrary skip (argument). + +trace :: #force_no_inline proc() -> (bt: Trace_Const) { + bt.len = _trace(bt.trace[:]) + return +} + +trace_n :: #force_no_inline proc(max_len: i32, allocator := context.allocator) -> Trace { + bt := make([]Trace_Entry, max_len, allocator) + n := _trace(bt[:]) + return bt[:n] +} + +trace_fill :: #force_no_inline proc(buf: Trace) -> int { + return _trace(buf) +} + +trace_n_destroy :: proc(b: Trace, allocator := context.allocator) { + delete(b, allocator) +} + +// Processes the message trying to get more/useful information. +// This adds file and line information if the program is running in debug mode. +// +// If an error is returned the original message will be the result and is save to use. +lines :: proc { + lines_n, + lines_const, +} + +lines_n :: proc(bt: Trace, allocator := context.allocator, temp_allocator := context.temp_allocator) -> (out: []Line, err: Lines_Error) { + return _lines(bt, allocator, temp_allocator) +} + +lines_const :: proc(bt: Trace_Const, allocator := context.allocator, temp_allocator := context.temp_allocator) -> (out: []Line, err: Lines_Error) { + bt := bt + return _lines(bt.trace[:bt.len], allocator, temp_allocator) +} + +lines_destroy :: proc(lines: []Line, allocator := context.allocator) { + _lines_destroy(lines, allocator) +} + +assertion_failure_proc :: proc(prefix, message: string, loc: runtime.Source_Code_Location) -> ! { + { + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() + + lines, err := lines(trace(), context.temp_allocator, context.temp_allocator) + if err != nil { + fmt.eprintf("could not get backtrace for assertion failure: %v\n", err) + } else { + fmt.eprintln("[back trace]") + print(lines, temp_allocator=context.temp_allocator) + } + } + + runtime.default_assertion_failure_proc(prefix, message, loc) +} + +register_segfault_handler :: proc() { + _register_segfault_handler() +} + +print :: proc(lines: []Line, padding := " ", w: Maybe(io.Writer) = nil, temp_allocator := context.temp_allocator) { + w := w.? or_else os.to_writer(os.stderr) + + tbl := table.init(&table.Table{}, temp_allocator, temp_allocator) + + for line in lines { + table.row(tbl, padding, line.symbol, " - ", line.location) + } + + table.build(tbl, table.unicode_width_proc) + + for row in 0.. (n: int) { + ctx: unw_context_t + cursor: unw_cursor_t + + ret: i32 + ret = unw_getcontext(&ctx) + assert(ret == 0) + ret = unw_init_local(&cursor, &ctx) + assert(ret == 0) + + // Skip this function's frame and the caller. + if unw_step(&cursor) <= 0 { return } + + pc: uintptr + for ; unw_step(&cursor) > 0 && n < len(buf); n += 1 { + ret = unw_get_reg(&cursor, .IP, &pc) + assert(ret == 0) + buf[n] = rawptr(pc) + } + + return +} + +@(private="package") +_lines_destroy :: proc(lines: []Line, allocator: runtime.Allocator) { + for line in lines { + delete(line.location, allocator) + delete(line.symbol, allocator) + } + delete(lines, allocator) +} + +@(private="package") +_lines :: proc(bt: Trace, allocator, _: runtime.Allocator) -> (out: []Line, err: Lines_Error) { + out = make([]Line, len(bt), allocator) + defer if err != nil { _lines_destroy(out, allocator) } + + symbolicator := CSSymbolicatorCreateWithPid(posix.getpid()) + defer CSRelease(symbolicator) + + for &msg, i in out { + symbol := CSSymbolicatorGetSymbolWithAddressAtTime(symbolicator, uintptr(bt[i]), CSNow) + info := CSSymbolicatorGetSourceInfoWithAddressAtTime(symbolicator, uintptr(bt[i]), CSNow) + + msg.symbol = strings.clone_from(CSSymbolGetName(symbol), allocator) + + // No debug info. + if CSIsNull(info) { + owner := CSSymbolGetSymbolOwner(symbol) + msg.location = strings.clone_from(CSSymbolOwnerGetPath(owner), allocator) + } else { + path := string(CSSourceInfoGetPath(info)) + location := strings.builder_make(allocator) + strings.write_string(&location, path) + when ODIN_ERROR_POS_STYLE == .Default { + strings.write_byte(&location, '(') + strings.write_int (&location, int(CSSourceInfoGetLineNumber(info))) + strings.write_byte(&location, ')') + } else when ODIN_ERROR_POS_STYLE == .Unix { + strings.write_byte(&location, ':') + strings.write_int (&location, int(CSSourceInfoGetLineNumber(info))) + } else { + #panic("unhandled ODIN_ERROR_POS_STYLE") + } + msg.location = strings.to_string(location) + } + } + return +} + +CSTypeRef :: struct { + csCppData: rawptr, + csCppObj: rawptr, +} + +CSSymbolicatorRef :: distinct CSTypeRef +CSSymbolRef :: distinct CSTypeRef +CSSourceInfoRef :: distinct CSTypeRef +CSSymbolOwnerRef :: distinct CSTypeRef + +CSNow :: 0x80000000 + +foreign symbolication { + @(link_name="CSIsNull") + _CSIsNull :: proc(ref: CSTypeRef) -> bool --- + @(link_name="CSRelease") + _CSRelease :: proc(ref: CSTypeRef) --- + + CSSymbolicatorCreateWithPid :: proc(pid: posix.pid_t) -> CSSymbolicatorRef --- + + CSSymbolicatorGetSymbolWithAddressAtTime :: proc(symbolicator: CSSymbolicatorRef, addr: uintptr, time: u64) -> CSSymbolRef --- + CSSymbolicatorGetSourceInfoWithAddressAtTime :: proc(symbolicator: CSSymbolicatorRef, adrr: uintptr, time: u64) -> CSSourceInfoRef --- + + CSSymbolGetName :: proc(symbol: CSSymbolRef) -> cstring --- + CSSymbolGetSymbolOwner :: proc(symbol: CSSymbolRef) -> CSSymbolOwnerRef --- + + CSSourceInfoGetPath :: proc(info: CSSourceInfoRef) -> cstring --- + CSSourceInfoGetLineNumber :: proc(info: CSSourceInfoRef) -> i32 --- + CSSourceInfoGetSymbol :: proc(info: CSSourceInfoRef) -> CSSymbolRef --- + + CSSymbolOwnerGetPath :: proc(owner: CSSymbolOwnerRef) -> cstring --- +} + +CSRelease :: #force_inline proc(ref: $T) { + _CSRelease(CSTypeRef(ref)) +} + +CSIsNull :: #force_inline proc(ref: $T) -> bool { + return _CSIsNull(CSTypeRef(ref)) +} + +// These could actually be smaller, but then we would have to define and check the size on each +// architecture, the sizes here are the largest they can be. +_LIBUNWIND_CONTEXT_SIZE :: 167 +_LIBUNWIND_CURSOR_SIZE :: 204 + +unw_context_t :: struct { + data: [_LIBUNWIND_CONTEXT_SIZE]u64, +} + +unw_cursor_t :: struct { + data: [_LIBUNWIND_CURSOR_SIZE]u64, +} + +// Cross-platform registers, each architecture has additional registers but these are enough for us. +Register :: enum i32 { + SP = -2, + IP = -1, +} + +foreign system { + unw_getcontext :: proc(ctx: ^unw_context_t) -> i32 --- + unw_init_local :: proc(cursor: ^unw_cursor_t, ctx: ^unw_context_t) -> i32 --- + unw_get_reg :: proc(cursor: ^unw_cursor_t, name: Register, reg: ^uintptr) -> i32 --- + unw_step :: proc(cursor: ^unw_cursor_t) -> i32 --- +} + +} diff --git a/libraries/back/back_linux.odin b/libraries/back/back_linux.odin new file mode 100644 index 0000000..081a270 --- /dev/null +++ b/libraries/back/back_linux.odin @@ -0,0 +1,169 @@ +#+vet explicit-allocators +#+private file +package back + +@require import "base:runtime" +@require import "base:intrinsics" + +@require import "core:c" +@require import "core:c/libc" +@require import "core:os" +@require import "core:strings" + +when !USE_FALLBACK { + +foreign import lib "system:c" + +@(private="package") +_Trace_Entry :: rawptr + +@(private="package") +_trace :: #force_no_inline proc(buf: Trace) -> (n: int) { + // In order to omit this function's frame and the caller, we alloca a temp buffer with 2 extra slots. + bigger_buf := ([^]Trace_Entry)(intrinsics.alloca((2 + len(buf)) * size_of(Trace_Entry), align_of(Trace_Entry)))[:len(buf)+2] + _n := int(backtrace(raw_data(bigger_buf), i32(len(bigger_buf)))) + if _n > 2 { + copy(buf, bigger_buf[2:]) + return _n-2 + } + + return 0 +} + +@(private="package") +_lines_destroy :: proc(msgs: []Line, allocator: runtime.Allocator) { + for msg in msgs { + delete(msg.location, allocator) + if msg.symbol != "??OOM" && msg.symbol != "??" { delete(msg.symbol, allocator) } + } + delete(msgs, allocator) +} + +@(private="package") +_lines :: proc(bt: Trace, allocator, temp_allocator: runtime.Allocator) -> (out: []Line, err: Lines_Error) { + msgs := backtrace_symbols(raw_data(bt), i32(len(bt)))[:len(bt)] + defer libc.free(raw_data(msgs)) + + out = make([]Line, len(bt), allocator) + defer if err != nil { _lines_destroy(out, allocator) } + + // Debug info is needed. + when !ODIN_DEBUG { + for msg, i in msgs { + location, mem_err := strings.clone_from(msg, allocator) + if mem_err != nil { return out, .Out_Of_Memory } + + out[i] = Line { + location = location, + symbol = "??", + } + } + return + } + + i := 0 + + command := make([dynamic]string, temp_allocator) + defer delete(command) + + if _, err := append(&command, ADDR2LINE_PATH, "--functions", "--exe", ""); err != nil { return out, .Out_Of_Memory } + + COMMAND_EXE_POS :: 3 + COMMAND_START_LEN :: 4 + + for msg in msgs { + exe, addr := parse_address(msg) or_return + if command[COMMAND_EXE_POS] == "" { + command[COMMAND_EXE_POS] = exe + } else if command[COMMAND_EXE_POS] != exe { + i += exec_and_fill(command[:], out[i:], msgs[i:], allocator, temp_allocator) or_return + + command[COMMAND_EXE_POS] = exe + resize(&command, COMMAND_START_LEN) + } + + if _, err := append(&command, addr); err != nil { return out, .Out_Of_Memory } + } + + if len(command) > COMMAND_START_LEN { + i += exec_and_fill(command[:], out[i:], msgs[i:], allocator, temp_allocator) or_return + } + + return + + // Parses the exe and address out of a backtrace line. + // Example: .../main(+0x20) [0x100000] -> .../main, +0x20, nil + parse_address :: proc(cmsg: cstring) -> (string, string, Lines_Error) { + msg := string(cmsg) + close_idx := strings.last_index_byte(msg, ')') + if close_idx < 1 { + return "", "", .Parse_Address_Fail + } + + open_idx := strings.last_index_byte(msg[:close_idx], '(') + if open_idx < 0 { + return "", "", .Parse_Address_Fail + } + + return msg[:open_idx], msg[open_idx+1:close_idx], nil + } + + exec_and_fill :: proc(command: []string, out: []Line, msgs: []cstring, allocator, temp_allocator: runtime.Allocator) -> (filled: int, err: Lines_Error) { + state, stdout, stderr, perr := os.process_exec({command = command}, temp_allocator) + defer delete(stdout, temp_allocator) + defer delete(stderr, temp_allocator) + + if perr != nil || !state.success { + return 0, .Addr2line_Process_Error + } + + count := len(command)-COMMAND_START_LEN + + sstdout := string(stdout) + for i in 0.. (string, Lines_Error) { + if !ok { return "", .Addr2line_Unexpected_EOF } + if line == "" { return "", .Addr2line_Output_Error } + + if line == "??" { + return "??", nil + } + + ret, err := strings.clone(strings.trim_right_space(line), allocator) + return ret, err == nil ? nil : .Out_Of_Memory + } + +} + +foreign lib { + backtrace :: proc(buffer: [^]rawptr, size: c.int) -> c.int --- + backtrace_symbols :: proc(buffer: [^]rawptr, size: c.int) -> [^]cstring --- + backtrace_symbols_fd :: proc(buffer: [^]rawptr, size: c.int, fd: ^libc.FILE) --- +} + +} diff --git a/libraries/back/back_other.odin b/libraries/back/back_other.odin new file mode 100644 index 0000000..e51ef4a --- /dev/null +++ b/libraries/back/back_other.odin @@ -0,0 +1,127 @@ +#+vet explicit-allocators +package back + +@require import "base:runtime" + +@require import "core:strings" + +when USE_FALLBACK { + +when ODIN_OPTIMIZATION_MODE == .None { + #panic("the `back` package's `other` mode requires at least `-o:minimal` to work (it requires `#force_inline` to actually be applied)") +} + +when ODIN_USE_SEPARATE_MODULES { + #panic("the `back` package's `other` mode requires `-use-single-module` to work (there are subtle instrumentation bugs to hunt down)") +} + +@(no_instrumentation) +other_instrumentation_enter :: #force_inline proc "contextless" (a, b: rawptr, loc: runtime.Source_Code_Location) { + _other_instrumentation_enter(a, b, loc) +} + +@(no_instrumentation) +other_instrumentation_exit :: #force_inline proc "contextless" (a, b: rawptr, loc: runtime.Source_Code_Location) { + _other_instrumentation_exit(a, b, loc) +} + +@(private="package") +_Trace_Entry :: runtime.Source_Code_Location + +@(private="package") +_trace :: #force_no_inline proc(buf: Trace) -> (n: int) { + lframe := frame + + // Omit this function's frame and the caller. + if lframe != nil { lframe = lframe.prev } + if lframe != nil { lframe = lframe.prev } + + for lframe != nil && n < len(buf) { + buf[n] = lframe.loc + + n += 1 + lframe = lframe.prev + } + + return +} + +@(private="package") +_lines_destroy :: proc(lines: []Line, allocator: runtime.Allocator) { + for line in lines { + delete(line.location, allocator) + } +} + +@(private="package") +_lines :: proc(bt: Trace, allocator, temp_allocator: runtime.Allocator) -> (out: []Line, err: Lines_Error) { + out = make([]Line, len(bt), allocator) + + for t, i in bt { + out[i].symbol = t.procedure + + location := strings.builder_make(allocator) + strings.write_string(&location, t.file_path) + when ODIN_ERROR_POS_STYLE == .Default { + strings.write_byte(&location, '(') + strings.write_int (&location, int(t.line)) + if t.column != 0 { + strings.write_byte(&location, ':') + strings.write_int (&location, int(t.column)) + } + strings.write_byte(&location, ')') + } else when ODIN_ERROR_POS_STYLE == .Unix { + strings.write_byte(&location, ':') + strings.write_int (&location, int(t.line)) + if t.column != 0 { + strings.write_byte(&location, ':') + strings.write_int (&location, int(t.column)) + } + } else { + #panic("unhandled ODIN_ERROR_POS_STYLE") + } + + out[i].location = strings.to_string(location) + } + + return +} + +@(private="file") +Frame :: struct { + prev: ^Frame, + loc: runtime.Source_Code_Location, +} + +@(thread_local, private="file") +frame: ^Frame + +when OTHER_CUSTOM_INSTRUMENTATION { + @(no_instrumentation, private="file") + _other_instrumentation_enter :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = &Frame{ + prev = frame, + loc = loc, + } + } + + @(no_instrumentation, private="file") + _other_instrumentation_exit :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = frame.prev + } +} else { + @(instrumentation_enter, private="file") + _other_instrumentation_enter :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = &Frame{ + prev = frame, + loc = loc, + } + } + + @(instrumentation_exit, private="file") + _other_instrumentation_exit :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = frame.prev + } +} + +} diff --git a/libraries/back/back_windows.odin b/libraries/back/back_windows.odin new file mode 100644 index 0000000..485f6d0 --- /dev/null +++ b/libraries/back/back_windows.odin @@ -0,0 +1,140 @@ +#+vet explicit-allocators +#+private +package back + +@require import "base:intrinsics" +@require import "base:runtime" + +@require import "core:io" +@require import "core:strings" +@require import "core:sync" +@require import "core:unicode/utf16" +@require import win "core:sys/windows" + +when !USE_FALLBACK { + +SYMOPT_DEFERRED_LOADS :: 0x00000004 + +_Trace_Entry :: uintptr + +_trace :: #force_no_inline proc(buf: Trace) -> (n: int) { + frame_count := win.RtlCaptureStackBackTrace(2, u32(len(buf)), ([^]rawptr)(raw_data(buf)), nil) + + for &frame in buf[:frame_count] { + // NOTE: Return address is one after the call instruction so subtract a byte to + // end up back inside the call instruction which is needed for SymFromAddr. + frame -= 1 + } + + return int(frame_count) +} + +_lines_destroy :: proc(lines: []Line, allocator: runtime.Allocator) { + for line in lines { + delete(line.location, allocator) + if line.symbol != "??" && line.symbol != "??OOM" { + delete(line.symbol, allocator) + } + } + delete(lines, allocator) +} + +_lines :: proc(bt: Trace, allocator, temp_allocator: runtime.Allocator) -> (out: []Line, err: Lines_Error) { + // Debug info is needed, if we call with out-of-date debug symbols it will return out-of-date info, so better to short-circuit right away. + when !ODIN_DEBUG { + out = make([]Line, len(bt), allocator) + for &line, i in out { + line.symbol = "??" + + location := strings.builder_make(allocator) + strings.write_string(&location, "0x") + strings.write_i64 (&location, i64(bt[i]), 16) + line.location = strings.to_string(location) + } + + return + } + + out = make([]Line, len(bt), allocator) + defer if err != nil { _lines_destroy(out, allocator) } + + process := win.GetCurrentProcess() + + sync.guard(&_win32_dbghelp_mutex) + + if !win.SymInitialize(process, nil, true) { + err = .Info_Not_Found + return + } + defer win.SymCleanup(process) + + win.SymSetOptions(win.SYMOPT_LOAD_LINES|SYMOPT_DEFERRED_LOADS) + + data: [size_of(win.SYMBOL_INFOW) + size_of([256]win.WCHAR)]byte + symbol := (^win.SYMBOL_INFOW)(&data[0]) + // The value of SizeOfStruct must be the size of the whole struct, + // not just the size of the pointer + symbol.SizeOfStruct = size_of(symbol^) + symbol.MaxNameLen = 255 + + for &line, i in out { + if win.SymFromAddrW(process, win.DWORD64(bt[i]), nil, symbol) { + symbol, mem_err := win.wstring_to_utf8(cstring16(&symbol.Name[0]), int(symbol.NameLen), allocator) + if mem_err != nil { + line.symbol = "??OOM" + } else if symbol == "??" { + delete(symbol, allocator) + line.symbol = "??" + } else { + line.symbol = symbol + } + } else { + line.symbol = "??" + } + + lineInfo: win.IMAGEHLP_LINE64 + lineInfo.SizeOfStruct = size_of(lineInfo) + if win.SymGetLineFromAddrW64(process, win.DWORD64(bt[i]), &{}, &lineInfo) { + location := strings.builder_make(allocator) + write_string16(&location, string16(lineInfo.FileName)) + when ODIN_ERROR_POS_STYLE == .Default { + strings.write_byte(&location, '(') + strings.write_int (&location, int(lineInfo.LineNumber)) + strings.write_byte(&location, ')') + } else when ODIN_ERROR_POS_STYLE == .Unix { + strings.write_byte(&location, ':') + strings.write_int (&location, int(lineInfo.LineNumber)) + } else { + #panic("unhandled ODIN_ERROR_POS_STYLE") + } + line.location = strings.to_string(location) + } else { + location := strings.builder_make(allocator) + strings.write_string(&location, "0x") + strings.write_i64 (&location, i64(bt[i]), 16) + line.location = strings.to_string(location) + } + } + + return +} + +write_string16 :: proc(b: ^strings.Builder, s: string16, loc := #caller_location) -> (n: int, err: io.Error) { + for i := 0; i < len(s); i += 1 { + r := rune(utf16.REPLACEMENT_CHAR) + + switch c := s[i]; { + case c < utf16._surr1, utf16._surr3 <= c: + r = rune(c) + case utf16._surr1 <= c && c < utf16._surr2 && i+1 < len(s) && + utf16._surr2 <= s[i+1] && s[i+1] < utf16._surr3: + r = utf16.decode_surrogate_pair(rune(c), rune(s[i+1])) + i += 1 + } + + n += strings.write_rune(b, rune(r)) or_return + } + return +} + +} diff --git a/libraries/back/signal_handler_other.odin b/libraries/back/signal_handler_other.odin new file mode 100644 index 0000000..37d1a94 --- /dev/null +++ b/libraries/back/signal_handler_other.odin @@ -0,0 +1,11 @@ +#+vet explicit-allocators +#+build !linux +#+build !darwin +#+build !netbsd +#+build !openbsd +#+build !freebsd +#+build !windows +package back + +@(private="package") +_register_segfault_handler :: proc() {} diff --git a/libraries/back/signal_handler_posix.odin b/libraries/back/signal_handler_posix.odin new file mode 100644 index 0000000..7066c33 --- /dev/null +++ b/libraries/back/signal_handler_posix.odin @@ -0,0 +1,37 @@ +#+vet explicit-allocators +#+build linux, darwin, netbsd, openbsd, freebsd +package back + +import "base:runtime" + +import "core:fmt" +import "core:mem" +import "core:sys/posix" + +@(private="package") +_register_segfault_handler :: proc() { + posix.signal(.SIGSEGV, proc "c" (code: posix.Signal) { + context = runtime.default_context() + + space: [16*mem.Kilobyte]byte + arena: mem.Arena + mem.arena_init(&arena, space[:]) + allocator := mem.arena_allocator(&arena) + + context.allocator = allocator + context.temp_allocator = allocator + + backtrace: { + lines, err := lines(trace(), allocator, allocator) + if err != nil { + fmt.eprintf("Exception (Code: %i)\nCould not get backtrace: %v\n", code, err) + break backtrace + } + + fmt.eprintf("Exception (Code: %i)\n[back trace]\n", code) + print(lines, temp_allocator=allocator) + } + + runtime.exit(int(code)) + }) +} diff --git a/libraries/back/signal_handler_windows.odin b/libraries/back/signal_handler_windows.odin new file mode 100644 index 0000000..de5b33a --- /dev/null +++ b/libraries/back/signal_handler_windows.odin @@ -0,0 +1,38 @@ +#+vet explicit-allocators +package back + +import "base:runtime" + +import "core:fmt" +import "core:mem" +import win "core:sys/windows" + +_register_segfault_handler :: proc() { + win.SetUnhandledExceptionFilter(proc "stdcall" (exception_info: ^win.EXCEPTION_POINTERS) -> win.LONG { + context = runtime.default_context() + + space: [16*mem.Kilobyte]byte + arena: mem.Arena + mem.arena_init(&arena, space[:]) + allocator := mem.arena_allocator(&arena) + + context.allocator = allocator + context.temp_allocator = allocator + + fmt.eprint("Exception ") + if exception_info.ExceptionRecord != nil { + fmt.eprintf("(Type: %x, Flags: %x)\n", exception_info.ExceptionRecord.ExceptionCode, exception_info.ExceptionRecord.ExceptionFlags) + } + + lines, err := lines(trace(), allocator, allocator) + if err != nil { + fmt.eprintln("Could not get backtrace: %v", err) + return win.EXCEPTION_CONTINUE_SEARCH + } + + fmt.eprintln("[back trace]") + print(lines, temp_allocator=allocator) + + return win.EXCEPTION_CONTINUE_SEARCH + }) +} diff --git a/libraries/snths_ui/example/back/LICENSE b/libraries/snths_ui/example/back/LICENSE new file mode 100644 index 0000000..d97adb1 --- /dev/null +++ b/libraries/snths_ui/example/back/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Laytan Laats + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libraries/snths_ui/example/back/README.md b/libraries/snths_ui/example/back/README.md new file mode 100644 index 0000000..50f25bf --- /dev/null +++ b/libraries/snths_ui/example/back/README.md @@ -0,0 +1,188 @@ +# Back + +Backtraces for Odin, see examples below and in the examples folder. + +To change the size (amount of stackframes to print) in places where this can't be set directly, you can use the `-define:BACKTRACE_SIZE=16`. + +## Targets + +### Windows, MacoOS & Linux + +These targets use debug information that's added by compiling with `-debug` to provide traces and give rough information when compiled without it. + +Performance is not impacted at all during normal running, just when you ask for a trace or lines. + +Per platform notes below: + +#### Windows + +For Windows support, all credit goes to [DaseinPhaos/pdb](https://github.com/DaseinPhaos/pdb). + +Windows is not able to get *any* information when not compiled with `-debug`. + +NOTE: The pdb package allocates a lot of stuff and does not really provide a way of deleting the allocations, so, before calling into the package, this package sets it to use the `context.temporary_allocator`. + +#### MacOS + +Uses a private framework for symbolication, and thus will not get through Apple's review process. + +#### Linux + +On Linux, the `addr2line` command is invoked, which comes pre-installed (maybe in binutils). + +The `addr2line` command can be changed by setting the `-define:BACK_ADDR2LINE_PATH=your/atos` flag. + +The path to the running binary is also needed for this command, this is `os.args[0]` by default and to my knowledge is always correct. +Nevertheless it can be changed with the `-define:BACK_PROGRAM=path/to/binary` flag. + +I am planning on rewriting this implementation to not require an external command like this by parsing the DWARF debug information manually. + +### Others + +Other targets use the instrumentation features of Odin to keep track of stack frames, this has a minimal impact on performance and size. + +Forcing the instrumentation based implementation on Windows, Linux and Darwin can be done with `-define:BACK_FORCE_FALLBACK=true`. + +NOTE: this implementation requires at least `-o:minimal` as it requires `#force_inline` procs to actually be inlined, this is not the case with `-o:none`. + +NOTE: the instrumentation features and WASM combination is a bit fragile and seems to only work on `-o:minimal` exclusively, this is almost certainly a codegen bug. + +## Manual + +```odin +package manual + +import "core:fmt" + +import back "../.." + +main :: proc() { + // Allocates for 16 frames. + bt := back.trace_n(16) + print(bt) + + // Or, doesn't allocate, returns `-define:BACKTRACE_SIZE` frames. + btc := back.trace() + print(btc.trace[:btc.len]) + + // Or, fill in a slice. + bt = make(back.Trace, 16) + bt = bt[:back.trace_fill(bt)] + print(bt) +} + +print :: proc(bt: back.Trace) { + lines, err := back.lines(bt) + if err != nil { + fmt.eprintf("Could not retrieve backtrace lines: %v\n", err) + } else { + defer back.lines_destroy(lines) + + fmt.eprintln("[back trace]") + back.print(lines) + } +} + +// [back trace] +// back.trace_n - /Users/laytan/projects/back/back.odin:59 +// manual.main - /Users/laytan/projects/back/examples/manual/main.odin:9 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// manual.main - /Users/laytan/projects/back/examples/manual/main.odin:13 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +// [back trace] +// back.trace_fill - /Users/laytan/projects/back/back.odin:64 +// manual.main - /Users/laytan/projects/back/examples/manual/main.odin:18 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +``` + +## Tracking Allocator + +```odin +package main + +import "back" + +_main :: proc() { + _ = new(int) + free(rawptr(uintptr(100))) +} + +main :: proc() { + track: back.Tracking_Allocator + back.tracking_allocator_init(&track, context.allocator) + defer back.tracking_allocator_destroy(&track) + + context.allocator = back.tracking_allocator(&track) + defer back.tracking_allocator_print_results(&track) + + _main() +} + +// /Users/laytan/projects/back/examples/allocator/main.odin(6:6) leaked 8b +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// back.tracking_allocator_proc - /Users/laytan/projects/back/allocator.odin:129 +// runtime.mem_alloc_bytes - /Users/laytan/third-party/Odin/core/runtime/internal.odin:141 +// runtime.new_aligned-13136 - /Users/laytan/third-party/Odin/core/runtime/core_builtin.odin:250 +// runtime.new-13097 - /Users/laytan/third-party/Odin/core/runtime/core_builtin.odin:246 +// main._main - /Users/laytan/projects/back/examples/allocator/main.odin:6 +// main.main - /Users/laytan/projects/back/examples/allocator/main.odin:18 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +// +// +// /Users/laytan/projects/back/examples/allocator/main.odin(7:2) allocation 64 was freed badly +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// back.tracking_allocator_proc - /Users/laytan/projects/back/allocator.odin:102 +// runtime.mem_free - /Users/laytan/third-party/Odin/core/runtime/internal.odin:162 +// main._main - /Users/laytan/projects/back/examples/allocator/main.odin:8 +// main.main - /Users/laytan/projects/back/examples/allocator/main.odin:18 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +``` + +## Printing a backtrace on assertion failures / panics + +```odin +package main + +import "back" + +main :: proc() { + context.assertion_failure_proc = back.assertion_failure_proc + assert(3 == 2) +} + +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// back.assertion_failure_proc - /Users/laytan/projects/back/back.odin:97 +// runtime.assert.internal-0 - /Users/laytan/third-party/Odin/core/runtime/core_builtin.odin:818 +// runtime.assert - /Users/laytan/third-party/Odin/core/runtime/core_builtin.odin:820 +// main.main - /Users/laytan/projects/back/examples/assert_backtrace/main.odin:8 +// main - /Users/laytan/third-party/Odin/core/runtime/entry_unix.odin:53 +// /Users/laytan/projects/back/examples/assert_backtrace/main.odin(7:5) runtime assertion +``` + +## Printing a backtrace on segmentation faults + +```odin +package main + +import "back" + +main :: proc() { + back.register_segfault_handler() + + ptr: ^int + bad := ptr^ + 2 + _ = bad +} + +// Segmentation Fault +// [back trace] +// back.trace - /Users/laytan/projects/back/back.odin:52 +// back.register_segfault_handler$anon-1 - /Users/laytan/projects/back/back.odin:114 +// ?? - ?? +// main.main - /Users/laytan/projects/back/examples/segfault/main.odin:8 +``` diff --git a/libraries/snths_ui/example/back/allocator.odin b/libraries/snths_ui/example/back/allocator.odin new file mode 100644 index 0000000..986e424 --- /dev/null +++ b/libraries/snths_ui/example/back/allocator.odin @@ -0,0 +1,230 @@ +#+vet explicit-allocators +package back + +import "base:runtime" + +import "core:fmt" +import "core:mem" +import "core:sync" + +// The backtrace tracking allocator is a similar allocator as the `core:mem` tracking allocator but keeps +// backtraces for each allocation. +// +// See examples/allocator for a usage snippet. +// +// Print results at the end using tracking_allocator_print_results(). +Tracking_Allocator :: struct { + backing: mem.Allocator, + internals_allocator: mem.Allocator, + allocation_map: map[rawptr]Tracking_Allocator_Entry, + bad_free_array: [dynamic]Tracking_Allocator_Bad_Free_Entry, + mutex: sync.Mutex, + clear_on_free_all: bool, +} + +Tracking_Allocator_Entry :: struct { + memory: rawptr, + size: int, + alignment: int, + mode: mem.Allocator_Mode, + err: mem.Allocator_Error, + location: runtime.Source_Code_Location, + backtrace: Trace_Const, +} + +Tracking_Allocator_Bad_Free_Entry :: struct { + memory: rawptr, + location: runtime.Source_Code_Location, + backtrace: Trace_Const, +} + +tracking_allocator_init :: proc( + t: ^Tracking_Allocator, + backing_allocator: mem.Allocator, + internals_allocator := context.allocator, +) { + t.backing = backing_allocator + t.internals_allocator = internals_allocator + t.allocation_map.allocator = internals_allocator + t.bad_free_array.allocator = internals_allocator + + if .Free_All in mem.query_features(t.backing) { + t.clear_on_free_all = true + } +} + +tracking_allocator_destroy :: proc(t: ^Tracking_Allocator) { + delete(t.allocation_map) + delete(t.bad_free_array) +} + +tracking_allocator_clear :: proc(t: ^Tracking_Allocator) { + sync.guard(&t.mutex) + + clear(&t.allocation_map) + clear(&t.bad_free_array) +} + +@(require_results) +tracking_allocator :: proc(data: ^Tracking_Allocator) -> mem.Allocator { + return mem.Allocator{data = data, procedure = tracking_allocator_proc} +} + +tracking_allocator_proc :: proc( + allocator_data: rawptr, + mode: mem.Allocator_Mode, + size, alignment: int, + old_memory: rawptr, + old_size: int, + loc := #caller_location, +) -> ( + result: []byte, + err: mem.Allocator_Error, +) { + data := (^Tracking_Allocator)(allocator_data) + + sync.mutex_guard(&data.mutex) + + if mode == .Query_Info { + info := (^mem.Allocator_Query_Info)(old_memory) + if info != nil && info.pointer != nil { + if entry, ok := data.allocation_map[info.pointer]; ok { + info.size = entry.size + info.alignment = entry.alignment + } + info.pointer = nil + } + + return + } + + if mode == .Free && old_memory != nil && old_memory not_in data.allocation_map { + append( + &data.bad_free_array, + Tracking_Allocator_Bad_Free_Entry{ + memory = old_memory, + location = loc, + backtrace = trace(), + }, + ) + } else { + result = data.backing.procedure( + data.backing.data, + mode, + size, + alignment, + old_memory, + old_size, + loc, + ) or_return + } + result_ptr := raw_data(result) + + if data.allocation_map.allocator.procedure == nil { + data.allocation_map.allocator = context.allocator + } + + switch mode { + case .Alloc, .Alloc_Non_Zeroed: + data.allocation_map[result_ptr] = Tracking_Allocator_Entry { + memory = result_ptr, + size = size, + mode = mode, + alignment = alignment, + err = err, + location = loc, + backtrace = trace(), + } + case .Free: + delete_key(&data.allocation_map, old_memory) + case .Free_All: + if data.clear_on_free_all { + clear_map(&data.allocation_map) + } + case .Resize, .Resize_Non_Zeroed: + if old_memory != result_ptr { + delete_key(&data.allocation_map, old_memory) + } + data.allocation_map[result_ptr] = Tracking_Allocator_Entry { + memory = result_ptr, + size = size, + mode = mode, + alignment = alignment, + err = err, + location = loc, + backtrace = trace(), + } + + case .Query_Features: + set := (^mem.Allocator_Mode_Set)(old_memory) + if set != nil { + set^ = { + .Alloc, + .Alloc_Non_Zeroed, + .Free, + .Free_All, + .Resize, + .Query_Features, + .Query_Info, + } + } + return nil, nil + + case .Query_Info: + unreachable() + } + + return +} + +tracking_allocator_print_results :: proc(t: ^Tracking_Allocator, temp_allocator := context.temp_allocator) { + i: int + ALLOCATOR_MAX_BACKTRACES :: 16 + + for _, leak in t.allocation_map { + fmt.eprintfln("\x1b[31m%v leaked %m\x1b[0m", leak.location, leak.size) + + defer i += 1 + if i > ALLOCATOR_MAX_BACKTRACES { + continue + } + + trace, err := lines(leak.backtrace, temp_allocator, temp_allocator) + defer lines_destroy(trace, temp_allocator) + + fmt.eprintln("[back trace]") + + if err != nil { + fmt.eprintfln("backtrace error: %v", err) + continue + } + + print(trace, temp_allocator=temp_allocator) + fmt.eprintln() + } + + for bad_free, _ in t.bad_free_array { + fmt.eprintfln( + "\x1b[31m%v allocation %p was freed badly\x1b[0m", + bad_free.location, + bad_free.memory, + ) + + defer i += 1 + if i > ALLOCATOR_MAX_BACKTRACES { + continue + } + + trace, err := lines(bad_free.backtrace, temp_allocator, temp_allocator) + defer lines_destroy(trace, temp_allocator) + + fmt.eprintln("[back trace]") + + if err != nil { + fmt.eprintf("backtrace error: %v\n", err) + continue + } + + print(trace, temp_allocator=temp_allocator) + } +} diff --git a/libraries/snths_ui/example/back/back.odin b/libraries/snths_ui/example/back/back.odin new file mode 100644 index 0000000..80a4136 --- /dev/null +++ b/libraries/snths_ui/example/back/back.odin @@ -0,0 +1,147 @@ +#+vet explicit-allocators +package back + +import "base:runtime" + +import "core:fmt" +import "core:io" +import "core:os" +import "core:sync" +import "core:text/table" + +// Size of a constant backtrace, as used by the tracking allocator for example. +BACKTRACE_SIZE :: #config(BACKTRACE_SIZE, 16) + +// For targets that do not have native support (using debug info), +// backtraces are done through instrumentation, Odin only allows one enter/exit instrumentation +// procedure though, so you can set this to true, add your own instrumentation procs, and have +// them call `back.other_instrumentation_enter` and `back.other_instrumentation_exit` to hook +// up the backtraces. +// The custom proc must have `#force_inline`. +OTHER_CUSTOM_INSTRUMENTATION :: #config(BACK_OTHER_CUSTOM_INSTRUMENTATION, false) + +// Force the fallback instrumentation based implementation instead of debug info based. +FORCE_FALLBACK :: #config(BACK_FORCE_FALLBACK, false) + +// Fallback requires a single module (subtle bugs with multiple modules and instrumentation in Odin), +// and at least -o:minimal (#force_inline has to actually inline). +_COULD_USE_FALLBACK_WITHOUT_ERROR :: !ODIN_USE_SEPARATE_MODULES && ODIN_OPTIMIZATION_MODE >= .Minimal + +// Use the fallback (instrumentation based) implementation: +// if it is forced, or it can be used without error and debug info is off, or if the target has no debug info based support. +USE_FALLBACK :: FORCE_FALLBACK || (_COULD_USE_FALLBACK_WITHOUT_ERROR && !ODIN_DEBUG) || (ODIN_OS != .Darwin && ODIN_OS != .Linux && ODIN_OS != .Windows) + +ADDR2LINE_PATH :: #config(TRACE_ADDR2LINE_PATH, "addr2line") + +Trace :: []Trace_Entry + +Trace_Const :: struct { + trace: [BACKTRACE_SIZE]Trace_Entry, + len: int, +} + +// Platform specific. +Trace_Entry :: _Trace_Entry + +Line :: struct { + location: string, + symbol: string, +} + +// TODO: improve errors. +Lines_Error :: enum { + None, + Parse_Address_Fail, + Addr2line_Unexpected_EOF, + Addr2line_Output_Error, + Addr2line_Unresolved, + Addr2line_Process_Error, + Out_Of_Memory, + Info_Not_Found, +} + +// TODO: arbitrary skip (argument). + +trace :: #force_no_inline proc() -> (bt: Trace_Const) { + bt.len = _trace(bt.trace[:]) + return +} + +trace_n :: #force_no_inline proc(max_len: i32, allocator := context.allocator) -> Trace { + bt := make([]Trace_Entry, max_len, allocator) + n := _trace(bt[:]) + return bt[:n] +} + +trace_fill :: #force_no_inline proc(buf: Trace) -> int { + return _trace(buf) +} + +trace_n_destroy :: proc(b: Trace, allocator := context.allocator) { + delete(b, allocator) +} + +// Processes the message trying to get more/useful information. +// This adds file and line information if the program is running in debug mode. +// +// If an error is returned the original message will be the result and is save to use. +lines :: proc { + lines_n, + lines_const, +} + +lines_n :: proc(bt: Trace, allocator := context.allocator, temp_allocator := context.temp_allocator) -> (out: []Line, err: Lines_Error) { + return _lines(bt, allocator, temp_allocator) +} + +lines_const :: proc(bt: Trace_Const, allocator := context.allocator, temp_allocator := context.temp_allocator) -> (out: []Line, err: Lines_Error) { + bt := bt + return _lines(bt.trace[:bt.len], allocator, temp_allocator) +} + +lines_destroy :: proc(lines: []Line, allocator := context.allocator) { + _lines_destroy(lines, allocator) +} + +assertion_failure_proc :: proc(prefix, message: string, loc: runtime.Source_Code_Location) -> ! { + { + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() + + lines, err := lines(trace(), context.temp_allocator, context.temp_allocator) + if err != nil { + fmt.eprintf("could not get backtrace for assertion failure: %v\n", err) + } else { + fmt.eprintln("[back trace]") + print(lines, temp_allocator=context.temp_allocator) + } + } + + runtime.default_assertion_failure_proc(prefix, message, loc) +} + +register_segfault_handler :: proc() { + _register_segfault_handler() +} + +print :: proc(lines: []Line, padding := " ", w: Maybe(io.Writer) = nil, temp_allocator := context.temp_allocator) { + w := w.? or_else os.to_writer(os.stderr) + + tbl := table.init(&table.Table{}, temp_allocator, temp_allocator) + + for line in lines { + table.row(tbl, padding, line.symbol, " - ", line.location) + } + + table.build(tbl, table.unicode_width_proc) + + for row in 0.. (n: int) { + ctx: unw_context_t + cursor: unw_cursor_t + + ret: i32 + ret = unw_getcontext(&ctx) + assert(ret == 0) + ret = unw_init_local(&cursor, &ctx) + assert(ret == 0) + + // Skip this function's frame and the caller. + if unw_step(&cursor) <= 0 { return } + + pc: uintptr + for ; unw_step(&cursor) > 0 && n < len(buf); n += 1 { + ret = unw_get_reg(&cursor, .IP, &pc) + assert(ret == 0) + buf[n] = rawptr(pc) + } + + return +} + +@(private="package") +_lines_destroy :: proc(lines: []Line, allocator: runtime.Allocator) { + for line in lines { + delete(line.location, allocator) + delete(line.symbol, allocator) + } + delete(lines, allocator) +} + +@(private="package") +_lines :: proc(bt: Trace, allocator, _: runtime.Allocator) -> (out: []Line, err: Lines_Error) { + out = make([]Line, len(bt), allocator) + defer if err != nil { _lines_destroy(out, allocator) } + + symbolicator := CSSymbolicatorCreateWithPid(posix.getpid()) + defer CSRelease(symbolicator) + + for &msg, i in out { + symbol := CSSymbolicatorGetSymbolWithAddressAtTime(symbolicator, uintptr(bt[i]), CSNow) + info := CSSymbolicatorGetSourceInfoWithAddressAtTime(symbolicator, uintptr(bt[i]), CSNow) + + msg.symbol = strings.clone_from(CSSymbolGetName(symbol), allocator) + + // No debug info. + if CSIsNull(info) { + owner := CSSymbolGetSymbolOwner(symbol) + msg.location = strings.clone_from(CSSymbolOwnerGetPath(owner), allocator) + } else { + path := string(CSSourceInfoGetPath(info)) + location := strings.builder_make(allocator) + strings.write_string(&location, path) + when ODIN_ERROR_POS_STYLE == .Default { + strings.write_byte(&location, '(') + strings.write_int (&location, int(CSSourceInfoGetLineNumber(info))) + strings.write_byte(&location, ')') + } else when ODIN_ERROR_POS_STYLE == .Unix { + strings.write_byte(&location, ':') + strings.write_int (&location, int(CSSourceInfoGetLineNumber(info))) + } else { + #panic("unhandled ODIN_ERROR_POS_STYLE") + } + msg.location = strings.to_string(location) + } + } + return +} + +CSTypeRef :: struct { + csCppData: rawptr, + csCppObj: rawptr, +} + +CSSymbolicatorRef :: distinct CSTypeRef +CSSymbolRef :: distinct CSTypeRef +CSSourceInfoRef :: distinct CSTypeRef +CSSymbolOwnerRef :: distinct CSTypeRef + +CSNow :: 0x80000000 + +foreign symbolication { + @(link_name="CSIsNull") + _CSIsNull :: proc(ref: CSTypeRef) -> bool --- + @(link_name="CSRelease") + _CSRelease :: proc(ref: CSTypeRef) --- + + CSSymbolicatorCreateWithPid :: proc(pid: posix.pid_t) -> CSSymbolicatorRef --- + + CSSymbolicatorGetSymbolWithAddressAtTime :: proc(symbolicator: CSSymbolicatorRef, addr: uintptr, time: u64) -> CSSymbolRef --- + CSSymbolicatorGetSourceInfoWithAddressAtTime :: proc(symbolicator: CSSymbolicatorRef, adrr: uintptr, time: u64) -> CSSourceInfoRef --- + + CSSymbolGetName :: proc(symbol: CSSymbolRef) -> cstring --- + CSSymbolGetSymbolOwner :: proc(symbol: CSSymbolRef) -> CSSymbolOwnerRef --- + + CSSourceInfoGetPath :: proc(info: CSSourceInfoRef) -> cstring --- + CSSourceInfoGetLineNumber :: proc(info: CSSourceInfoRef) -> i32 --- + CSSourceInfoGetSymbol :: proc(info: CSSourceInfoRef) -> CSSymbolRef --- + + CSSymbolOwnerGetPath :: proc(owner: CSSymbolOwnerRef) -> cstring --- +} + +CSRelease :: #force_inline proc(ref: $T) { + _CSRelease(CSTypeRef(ref)) +} + +CSIsNull :: #force_inline proc(ref: $T) -> bool { + return _CSIsNull(CSTypeRef(ref)) +} + +// These could actually be smaller, but then we would have to define and check the size on each +// architecture, the sizes here are the largest they can be. +_LIBUNWIND_CONTEXT_SIZE :: 167 +_LIBUNWIND_CURSOR_SIZE :: 204 + +unw_context_t :: struct { + data: [_LIBUNWIND_CONTEXT_SIZE]u64, +} + +unw_cursor_t :: struct { + data: [_LIBUNWIND_CURSOR_SIZE]u64, +} + +// Cross-platform registers, each architecture has additional registers but these are enough for us. +Register :: enum i32 { + SP = -2, + IP = -1, +} + +foreign system { + unw_getcontext :: proc(ctx: ^unw_context_t) -> i32 --- + unw_init_local :: proc(cursor: ^unw_cursor_t, ctx: ^unw_context_t) -> i32 --- + unw_get_reg :: proc(cursor: ^unw_cursor_t, name: Register, reg: ^uintptr) -> i32 --- + unw_step :: proc(cursor: ^unw_cursor_t) -> i32 --- +} + +} diff --git a/libraries/snths_ui/example/back/back_linux.odin b/libraries/snths_ui/example/back/back_linux.odin new file mode 100644 index 0000000..081a270 --- /dev/null +++ b/libraries/snths_ui/example/back/back_linux.odin @@ -0,0 +1,169 @@ +#+vet explicit-allocators +#+private file +package back + +@require import "base:runtime" +@require import "base:intrinsics" + +@require import "core:c" +@require import "core:c/libc" +@require import "core:os" +@require import "core:strings" + +when !USE_FALLBACK { + +foreign import lib "system:c" + +@(private="package") +_Trace_Entry :: rawptr + +@(private="package") +_trace :: #force_no_inline proc(buf: Trace) -> (n: int) { + // In order to omit this function's frame and the caller, we alloca a temp buffer with 2 extra slots. + bigger_buf := ([^]Trace_Entry)(intrinsics.alloca((2 + len(buf)) * size_of(Trace_Entry), align_of(Trace_Entry)))[:len(buf)+2] + _n := int(backtrace(raw_data(bigger_buf), i32(len(bigger_buf)))) + if _n > 2 { + copy(buf, bigger_buf[2:]) + return _n-2 + } + + return 0 +} + +@(private="package") +_lines_destroy :: proc(msgs: []Line, allocator: runtime.Allocator) { + for msg in msgs { + delete(msg.location, allocator) + if msg.symbol != "??OOM" && msg.symbol != "??" { delete(msg.symbol, allocator) } + } + delete(msgs, allocator) +} + +@(private="package") +_lines :: proc(bt: Trace, allocator, temp_allocator: runtime.Allocator) -> (out: []Line, err: Lines_Error) { + msgs := backtrace_symbols(raw_data(bt), i32(len(bt)))[:len(bt)] + defer libc.free(raw_data(msgs)) + + out = make([]Line, len(bt), allocator) + defer if err != nil { _lines_destroy(out, allocator) } + + // Debug info is needed. + when !ODIN_DEBUG { + for msg, i in msgs { + location, mem_err := strings.clone_from(msg, allocator) + if mem_err != nil { return out, .Out_Of_Memory } + + out[i] = Line { + location = location, + symbol = "??", + } + } + return + } + + i := 0 + + command := make([dynamic]string, temp_allocator) + defer delete(command) + + if _, err := append(&command, ADDR2LINE_PATH, "--functions", "--exe", ""); err != nil { return out, .Out_Of_Memory } + + COMMAND_EXE_POS :: 3 + COMMAND_START_LEN :: 4 + + for msg in msgs { + exe, addr := parse_address(msg) or_return + if command[COMMAND_EXE_POS] == "" { + command[COMMAND_EXE_POS] = exe + } else if command[COMMAND_EXE_POS] != exe { + i += exec_and_fill(command[:], out[i:], msgs[i:], allocator, temp_allocator) or_return + + command[COMMAND_EXE_POS] = exe + resize(&command, COMMAND_START_LEN) + } + + if _, err := append(&command, addr); err != nil { return out, .Out_Of_Memory } + } + + if len(command) > COMMAND_START_LEN { + i += exec_and_fill(command[:], out[i:], msgs[i:], allocator, temp_allocator) or_return + } + + return + + // Parses the exe and address out of a backtrace line. + // Example: .../main(+0x20) [0x100000] -> .../main, +0x20, nil + parse_address :: proc(cmsg: cstring) -> (string, string, Lines_Error) { + msg := string(cmsg) + close_idx := strings.last_index_byte(msg, ')') + if close_idx < 1 { + return "", "", .Parse_Address_Fail + } + + open_idx := strings.last_index_byte(msg[:close_idx], '(') + if open_idx < 0 { + return "", "", .Parse_Address_Fail + } + + return msg[:open_idx], msg[open_idx+1:close_idx], nil + } + + exec_and_fill :: proc(command: []string, out: []Line, msgs: []cstring, allocator, temp_allocator: runtime.Allocator) -> (filled: int, err: Lines_Error) { + state, stdout, stderr, perr := os.process_exec({command = command}, temp_allocator) + defer delete(stdout, temp_allocator) + defer delete(stderr, temp_allocator) + + if perr != nil || !state.success { + return 0, .Addr2line_Process_Error + } + + count := len(command)-COMMAND_START_LEN + + sstdout := string(stdout) + for i in 0.. (string, Lines_Error) { + if !ok { return "", .Addr2line_Unexpected_EOF } + if line == "" { return "", .Addr2line_Output_Error } + + if line == "??" { + return "??", nil + } + + ret, err := strings.clone(strings.trim_right_space(line), allocator) + return ret, err == nil ? nil : .Out_Of_Memory + } + +} + +foreign lib { + backtrace :: proc(buffer: [^]rawptr, size: c.int) -> c.int --- + backtrace_symbols :: proc(buffer: [^]rawptr, size: c.int) -> [^]cstring --- + backtrace_symbols_fd :: proc(buffer: [^]rawptr, size: c.int, fd: ^libc.FILE) --- +} + +} diff --git a/libraries/snths_ui/example/back/back_other.odin b/libraries/snths_ui/example/back/back_other.odin new file mode 100644 index 0000000..e51ef4a --- /dev/null +++ b/libraries/snths_ui/example/back/back_other.odin @@ -0,0 +1,127 @@ +#+vet explicit-allocators +package back + +@require import "base:runtime" + +@require import "core:strings" + +when USE_FALLBACK { + +when ODIN_OPTIMIZATION_MODE == .None { + #panic("the `back` package's `other` mode requires at least `-o:minimal` to work (it requires `#force_inline` to actually be applied)") +} + +when ODIN_USE_SEPARATE_MODULES { + #panic("the `back` package's `other` mode requires `-use-single-module` to work (there are subtle instrumentation bugs to hunt down)") +} + +@(no_instrumentation) +other_instrumentation_enter :: #force_inline proc "contextless" (a, b: rawptr, loc: runtime.Source_Code_Location) { + _other_instrumentation_enter(a, b, loc) +} + +@(no_instrumentation) +other_instrumentation_exit :: #force_inline proc "contextless" (a, b: rawptr, loc: runtime.Source_Code_Location) { + _other_instrumentation_exit(a, b, loc) +} + +@(private="package") +_Trace_Entry :: runtime.Source_Code_Location + +@(private="package") +_trace :: #force_no_inline proc(buf: Trace) -> (n: int) { + lframe := frame + + // Omit this function's frame and the caller. + if lframe != nil { lframe = lframe.prev } + if lframe != nil { lframe = lframe.prev } + + for lframe != nil && n < len(buf) { + buf[n] = lframe.loc + + n += 1 + lframe = lframe.prev + } + + return +} + +@(private="package") +_lines_destroy :: proc(lines: []Line, allocator: runtime.Allocator) { + for line in lines { + delete(line.location, allocator) + } +} + +@(private="package") +_lines :: proc(bt: Trace, allocator, temp_allocator: runtime.Allocator) -> (out: []Line, err: Lines_Error) { + out = make([]Line, len(bt), allocator) + + for t, i in bt { + out[i].symbol = t.procedure + + location := strings.builder_make(allocator) + strings.write_string(&location, t.file_path) + when ODIN_ERROR_POS_STYLE == .Default { + strings.write_byte(&location, '(') + strings.write_int (&location, int(t.line)) + if t.column != 0 { + strings.write_byte(&location, ':') + strings.write_int (&location, int(t.column)) + } + strings.write_byte(&location, ')') + } else when ODIN_ERROR_POS_STYLE == .Unix { + strings.write_byte(&location, ':') + strings.write_int (&location, int(t.line)) + if t.column != 0 { + strings.write_byte(&location, ':') + strings.write_int (&location, int(t.column)) + } + } else { + #panic("unhandled ODIN_ERROR_POS_STYLE") + } + + out[i].location = strings.to_string(location) + } + + return +} + +@(private="file") +Frame :: struct { + prev: ^Frame, + loc: runtime.Source_Code_Location, +} + +@(thread_local, private="file") +frame: ^Frame + +when OTHER_CUSTOM_INSTRUMENTATION { + @(no_instrumentation, private="file") + _other_instrumentation_enter :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = &Frame{ + prev = frame, + loc = loc, + } + } + + @(no_instrumentation, private="file") + _other_instrumentation_exit :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = frame.prev + } +} else { + @(instrumentation_enter, private="file") + _other_instrumentation_enter :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = &Frame{ + prev = frame, + loc = loc, + } + } + + @(instrumentation_exit, private="file") + _other_instrumentation_exit :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = frame.prev + } +} + +} diff --git a/libraries/snths_ui/example/back/back_windows.odin b/libraries/snths_ui/example/back/back_windows.odin new file mode 100644 index 0000000..485f6d0 --- /dev/null +++ b/libraries/snths_ui/example/back/back_windows.odin @@ -0,0 +1,140 @@ +#+vet explicit-allocators +#+private +package back + +@require import "base:intrinsics" +@require import "base:runtime" + +@require import "core:io" +@require import "core:strings" +@require import "core:sync" +@require import "core:unicode/utf16" +@require import win "core:sys/windows" + +when !USE_FALLBACK { + +SYMOPT_DEFERRED_LOADS :: 0x00000004 + +_Trace_Entry :: uintptr + +_trace :: #force_no_inline proc(buf: Trace) -> (n: int) { + frame_count := win.RtlCaptureStackBackTrace(2, u32(len(buf)), ([^]rawptr)(raw_data(buf)), nil) + + for &frame in buf[:frame_count] { + // NOTE: Return address is one after the call instruction so subtract a byte to + // end up back inside the call instruction which is needed for SymFromAddr. + frame -= 1 + } + + return int(frame_count) +} + +_lines_destroy :: proc(lines: []Line, allocator: runtime.Allocator) { + for line in lines { + delete(line.location, allocator) + if line.symbol != "??" && line.symbol != "??OOM" { + delete(line.symbol, allocator) + } + } + delete(lines, allocator) +} + +_lines :: proc(bt: Trace, allocator, temp_allocator: runtime.Allocator) -> (out: []Line, err: Lines_Error) { + // Debug info is needed, if we call with out-of-date debug symbols it will return out-of-date info, so better to short-circuit right away. + when !ODIN_DEBUG { + out = make([]Line, len(bt), allocator) + for &line, i in out { + line.symbol = "??" + + location := strings.builder_make(allocator) + strings.write_string(&location, "0x") + strings.write_i64 (&location, i64(bt[i]), 16) + line.location = strings.to_string(location) + } + + return + } + + out = make([]Line, len(bt), allocator) + defer if err != nil { _lines_destroy(out, allocator) } + + process := win.GetCurrentProcess() + + sync.guard(&_win32_dbghelp_mutex) + + if !win.SymInitialize(process, nil, true) { + err = .Info_Not_Found + return + } + defer win.SymCleanup(process) + + win.SymSetOptions(win.SYMOPT_LOAD_LINES|SYMOPT_DEFERRED_LOADS) + + data: [size_of(win.SYMBOL_INFOW) + size_of([256]win.WCHAR)]byte + symbol := (^win.SYMBOL_INFOW)(&data[0]) + // The value of SizeOfStruct must be the size of the whole struct, + // not just the size of the pointer + symbol.SizeOfStruct = size_of(symbol^) + symbol.MaxNameLen = 255 + + for &line, i in out { + if win.SymFromAddrW(process, win.DWORD64(bt[i]), nil, symbol) { + symbol, mem_err := win.wstring_to_utf8(cstring16(&symbol.Name[0]), int(symbol.NameLen), allocator) + if mem_err != nil { + line.symbol = "??OOM" + } else if symbol == "??" { + delete(symbol, allocator) + line.symbol = "??" + } else { + line.symbol = symbol + } + } else { + line.symbol = "??" + } + + lineInfo: win.IMAGEHLP_LINE64 + lineInfo.SizeOfStruct = size_of(lineInfo) + if win.SymGetLineFromAddrW64(process, win.DWORD64(bt[i]), &{}, &lineInfo) { + location := strings.builder_make(allocator) + write_string16(&location, string16(lineInfo.FileName)) + when ODIN_ERROR_POS_STYLE == .Default { + strings.write_byte(&location, '(') + strings.write_int (&location, int(lineInfo.LineNumber)) + strings.write_byte(&location, ')') + } else when ODIN_ERROR_POS_STYLE == .Unix { + strings.write_byte(&location, ':') + strings.write_int (&location, int(lineInfo.LineNumber)) + } else { + #panic("unhandled ODIN_ERROR_POS_STYLE") + } + line.location = strings.to_string(location) + } else { + location := strings.builder_make(allocator) + strings.write_string(&location, "0x") + strings.write_i64 (&location, i64(bt[i]), 16) + line.location = strings.to_string(location) + } + } + + return +} + +write_string16 :: proc(b: ^strings.Builder, s: string16, loc := #caller_location) -> (n: int, err: io.Error) { + for i := 0; i < len(s); i += 1 { + r := rune(utf16.REPLACEMENT_CHAR) + + switch c := s[i]; { + case c < utf16._surr1, utf16._surr3 <= c: + r = rune(c) + case utf16._surr1 <= c && c < utf16._surr2 && i+1 < len(s) && + utf16._surr2 <= s[i+1] && s[i+1] < utf16._surr3: + r = utf16.decode_surrogate_pair(rune(c), rune(s[i+1])) + i += 1 + } + + n += strings.write_rune(b, rune(r)) or_return + } + return +} + +} diff --git a/libraries/snths_ui/example/back/signal_handler_other.odin b/libraries/snths_ui/example/back/signal_handler_other.odin new file mode 100644 index 0000000..37d1a94 --- /dev/null +++ b/libraries/snths_ui/example/back/signal_handler_other.odin @@ -0,0 +1,11 @@ +#+vet explicit-allocators +#+build !linux +#+build !darwin +#+build !netbsd +#+build !openbsd +#+build !freebsd +#+build !windows +package back + +@(private="package") +_register_segfault_handler :: proc() {} diff --git a/libraries/snths_ui/example/back/signal_handler_posix.odin b/libraries/snths_ui/example/back/signal_handler_posix.odin new file mode 100644 index 0000000..7066c33 --- /dev/null +++ b/libraries/snths_ui/example/back/signal_handler_posix.odin @@ -0,0 +1,37 @@ +#+vet explicit-allocators +#+build linux, darwin, netbsd, openbsd, freebsd +package back + +import "base:runtime" + +import "core:fmt" +import "core:mem" +import "core:sys/posix" + +@(private="package") +_register_segfault_handler :: proc() { + posix.signal(.SIGSEGV, proc "c" (code: posix.Signal) { + context = runtime.default_context() + + space: [16*mem.Kilobyte]byte + arena: mem.Arena + mem.arena_init(&arena, space[:]) + allocator := mem.arena_allocator(&arena) + + context.allocator = allocator + context.temp_allocator = allocator + + backtrace: { + lines, err := lines(trace(), allocator, allocator) + if err != nil { + fmt.eprintf("Exception (Code: %i)\nCould not get backtrace: %v\n", code, err) + break backtrace + } + + fmt.eprintf("Exception (Code: %i)\n[back trace]\n", code) + print(lines, temp_allocator=allocator) + } + + runtime.exit(int(code)) + }) +} diff --git a/libraries/snths_ui/example/back/signal_handler_windows.odin b/libraries/snths_ui/example/back/signal_handler_windows.odin new file mode 100644 index 0000000..de5b33a --- /dev/null +++ b/libraries/snths_ui/example/back/signal_handler_windows.odin @@ -0,0 +1,38 @@ +#+vet explicit-allocators +package back + +import "base:runtime" + +import "core:fmt" +import "core:mem" +import win "core:sys/windows" + +_register_segfault_handler :: proc() { + win.SetUnhandledExceptionFilter(proc "stdcall" (exception_info: ^win.EXCEPTION_POINTERS) -> win.LONG { + context = runtime.default_context() + + space: [16*mem.Kilobyte]byte + arena: mem.Arena + mem.arena_init(&arena, space[:]) + allocator := mem.arena_allocator(&arena) + + context.allocator = allocator + context.temp_allocator = allocator + + fmt.eprint("Exception ") + if exception_info.ExceptionRecord != nil { + fmt.eprintf("(Type: %x, Flags: %x)\n", exception_info.ExceptionRecord.ExceptionCode, exception_info.ExceptionRecord.ExceptionFlags) + } + + lines, err := lines(trace(), allocator, allocator) + if err != nil { + fmt.eprintln("Could not get backtrace: %v", err) + return win.EXCEPTION_CONTINUE_SEARCH + } + + fmt.eprintln("[back trace]") + print(lines, temp_allocator=allocator) + + return win.EXCEPTION_CONTINUE_SEARCH + }) +} diff --git a/libraries/snths_ui/example/main.odin b/libraries/snths_ui/example/main.odin new file mode 100644 index 0000000..d72892f --- /dev/null +++ b/libraries/snths_ui/example/main.odin @@ -0,0 +1,116 @@ +package main + +import rl "vendor:raylib" +import ui ".." +import "core:fmt" + +WINDOW_WIDTH :: 1280 +WINDOW_HEIGHT :: 720 +WINDOW_CAPTION :: "Immediate mode GUI" +WINDOW_FRAMERATE :: 60 + +v2 :: [2]f32 +V2_ONE :: v2{1.0, 1.0} + +main :: proc() { + // rl.SetTraceLogLevel(.WARNING) + rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_CAPTION) + defer rl.CloseWindow() + rl.SetTargetFPS(WINDOW_FRAMERATE) + + ctx: ui.Context + ui.init(&ctx, 4000) + + outer_container_config := ui.ElementConfiguration {margin = {4.0, 8.0}, padding = {12.0, 24.0}, color = ui.col(rl.RED), label = ""} + inner_container_config := ui.ElementConfiguration {color = cast(ui.col)rl.GREEN, label = "", margin = {4.0, 4.0}} + button_config := ui.ElementConfiguration {margin = {4.0, 4.0}} + + for !rl.WindowShouldClose() { + + ui.start(&ctx, origin = {0, 0}, size = {WINDOW_WIDTH, WINDOW_HEIGHT}) + for i in 0..<7 { + ui.container_begin(dimensions = {ui.SIZE_AUTO, ui.Size_Percentage(0.1)}, config = inner_container_config) + for j in 0..<10 { + config := button_config + config.label = fmt.ctprintf("%d", i * 10 + j) + ui.button(dimensions = {ui.Size_Pixels(64.0), ui.Size_Percentage(1.0)}, config = config) + ui.same_line() + } + ui.container_end() + } + + ui.container_begin(dimensions = {ui.SIZE_AUTO, ui.Size_Percentage(0.2)}, config = inner_container_config) + for j in 0..<10 { + config := button_config + config.label = fmt.ctprintf("%d", 70 + j) + ui.button(dimensions = {ui.Size_Pixels(64.0), ui.Size_Percentage(0.5)}, config = config) + ui.same_line() + } + ctx.same_line = false + for j in 0..<10 { + config := button_config + config.label = fmt.ctprintf("%d", 80 + j) + ui.button(dimensions = {ui.Size_Pixels(64.0), ui.Size_Percentage(0.5)}, config = config) + ui.same_line() + } + ui.container_end() + + ui.container_begin(dimensions = {ui.Size_Percentage(1.0), ui.Size_PercentageRemainder(1.0)}, config = inner_container_config) + for j in 0..<10 { + config := button_config + config.label = fmt.ctprintf("%d", 90 + j) + ui.button(dimensions = {ui.Size_Percentage(0.1), ui.Size_Percentage(1.0)}, config = config) + ui.same_line() + } + ui.container_end() + elements := ui.end() + + rl.BeginDrawing() + defer rl.EndDrawing() + rl.ClearBackground(rl.BLACK) + + render_ui_elements(elements) + + ui.start(&ctx, {0, 0}, {WINDOW_WIDTH, WINDOW_HEIGHT}) + config := ui.ElementConfiguration{margin = V2_ONE * 4, padding = V2_ONE * 4, color = ui.col(rl.MAGENTA)} + ui.container_begin({ui.Size_Percentage(0.3), ui.Size_Percentage(0.3)}, config) + slider_config := config + slider_config.label = "Outer container margin" + ui_slider_2d(1.0, 0.2, slider_config, &outer_container_config.margin, 0.0, 32.0) + slider_config.label = "Inner container margin" + ui_slider_2d(1.0, 0.2, slider_config, &inner_container_config.margin, 0.0, 32.0) + slider_config.label = "Button margin" + ui_slider_2d(1.0, 0.2, slider_config, &button_config.margin, 0.0, 32.0) + ui.container_end() + debug_elements := ui.end() + + render_ui_elements(debug_elements) + } +} + +ui_slider_2d :: proc(width, height: ui.Size_Percentage, config: ui.ElementConfiguration, value: ^v2, min: f32, max: f32) -> ^ui.Element { + container := ui.container_begin(dimensions = {width, height}, config = config) + dimensions := ui.Dimensions {ui.Size_Percentage(0.5), ui.Size_Percentage(1.0)} + ui.slider(dimensions, {margin = config.margin}, {value = &value.x, min = min, max = max}) + ui.same_line() + ui.slider(dimensions, {margin = config.margin}, {value = &value.y, min = min, max = max}) + ui.container_end() + return container +} + +render_ui_elements :: proc(elements: []ui.Element) { + for element in elements { + area := rl.Rectangle(element.area) + switch element.type { + case .Container: + rl.DrawRectangleRounded(area, 0.1, 4.0, {0, 0, 0, 192}) + rl.GuiGroupBox(area, element.label) + case .Button: + rl.GuiButton(area, element.label) + case .Slider: + data := element.data.float_input_data + rl.GuiSlider(area, "", "", data.value, data.min, data.max) + case .InputFloat: + } + } +} diff --git a/libraries/snths_ui/example/main_linux.odin b/libraries/snths_ui/example/main_linux.odin new file mode 100644 index 0000000..7c8fdc2 --- /dev/null +++ b/libraries/snths_ui/example/main_linux.odin @@ -0,0 +1,38 @@ +#+build linux, darwin, netbsd, openbsd, freebsd + +package main + +import "back" +import "base:runtime" + +import "core:fmt" +import "core:mem" +import "core:sys/posix" + +@(private="package") +_register_illegal_instruction_handler :: proc() { + posix.signal(.SIGILL, proc "c" (code: posix.Signal) { + context = runtime.default_context() + + space: [16*mem.Kilobyte]byte + arena: mem.Arena + mem.arena_init(&arena, space[:]) + allocator := mem.arena_allocator(&arena) + + context.allocator = allocator + context.temp_allocator = allocator + + backtrace: { + lines, err := back.lines(back.trace(), allocator, allocator) + if err != nil { + fmt.eprintf("Exception (Code: %i)\nCould not get backtrace: %v\n", code, err) + break backtrace + } + + fmt.eprintf("Exception (Code: %i)\n[back trace]\n", code) + back.print(lines, temp_allocator=allocator) + } + + runtime.exit(int(code)) + }) +} diff --git a/libraries/snths_ui/example/main_other.odin b/libraries/snths_ui/example/main_other.odin new file mode 100644 index 0000000..a9f000e --- /dev/null +++ b/libraries/snths_ui/example/main_other.odin @@ -0,0 +1,11 @@ +#+vet explicit-allocators +#+build !linux +#+build !darwin +#+build !netbsd +#+build !openbsd +#+build !freebsd +#+build !windows +package main + +@(private="package") +_register_illegal_instruction_handler :: proc() {} diff --git a/libraries/snths_ui/ui.odin b/libraries/snths_ui/ui.odin new file mode 100644 index 0000000..8866792 --- /dev/null +++ b/libraries/snths_ui/ui.odin @@ -0,0 +1,265 @@ +package synthas_ui + +v2 :: [2]f32 +rect2 :: struct {x, y, width, height: f32} +col :: [4]u8 + +SIZE_AUTO :: Size_Auto {} + +rect_get_tl :: #force_inline proc(rect: rect2) -> v2 {return {rect.x, rect.y}} +rect_get_tr :: #force_inline proc(rect: rect2) -> v2 {return {rect.x + rect.width, rect.y}} +rect_get_br :: #force_inline proc(rect: rect2) -> v2 {return {rect.x + rect.width, rect.y + rect.height}} +rect_get_bl :: #force_inline proc(rect: rect2) -> v2 {return {rect.x, rect.y + rect.height}} +rect_get_size :: #force_inline proc(rect: rect2) -> v2 {return {rect.width, rect.height}} +rect_grow :: proc(rect: rect2, amount: v2) -> rect2 { + return {rect.x - amount.x / 2.0, rect.y - amount.y / 2.0, rect.width + amount.x, rect.height + amount.y} +} + +ctx: ^Context + +Context :: struct { + origin: v2, + containers: [dynamic]int, + container_last_element: [dynamic]int, + elements: [dynamic]Element, + same_line: bool, +} + +CONTAINER_STACK_SIZE :: 16 + +Element :: struct { + area: rect2, + using config: ElementConfiguration, + data: ElementData, + type: ElementType, + dimensions: Dimensions, +} + +ElementConfiguration :: struct { + label: cstring, + color: col, + margin, padding: v2, +} + +ElementData :: struct { + using float_input_data: ElementData_FloatInput, +} + +ElementData_FloatInput :: struct { + min, max: f32, + value: ^f32, +} + +ElementType :: enum { + Container, + Button, + Slider, + InputFloat, +} + +Size_Pixels :: distinct f32 +Size_Percentage :: distinct f32 +Size_PercentageRemainder :: distinct f32 +Size_Auto :: distinct bool + +Size :: union #no_nil { + Size_Pixels, + Size_Percentage, + Size_PercentageRemainder, + Size_Auto, +} + +Dimensions :: struct { + width, height: Size, +} + +Axis :: enum {X, Y} + +init :: proc(state: ^Context, element_capacity: int, allocator := context.allocator) { + state.containers = make(type_of(state.containers), 0, CONTAINER_STACK_SIZE, allocator) + state.container_last_element = make(type_of(state.container_last_element), 0, CONTAINER_STACK_SIZE, allocator) + state.elements = make(type_of(state.elements), 0, element_capacity, allocator) +} + +start :: proc(ui_context: ^Context, origin: v2, size: v2) { + ctx = ui_context + ctx.origin = origin + clear(&ctx.elements) + clear(&ctx.containers) + clear(&ctx.container_last_element) + append(&ctx.elements, Element{area = {**origin, **size}, type = .Container}) + append(&ctx.containers, 0) + append(&ctx.container_last_element, -1) +} + +end :: proc() -> []Element { + assert(len(ctx.containers) < 2, "Didn't close all containers before ending UI layout mode") + assert(len(ctx.containers) > 0, "Closed base container before ending UI layout mode") + elements := ctx.elements[:] + ctx = nil + return elements[1:] +} + +same_line :: proc() { + ctx.same_line = true +} + +container_begin :: proc(dimensions: Dimensions, config: ElementConfiguration) -> ^Element { + area := _resolve_element_area(dimensions, config.margin) + element := _append_element({area = area, config = config, type = .Container, dimensions = dimensions}) + container_index := len(ctx.elements) - 1 + append(&ctx.containers, container_index) + append(&ctx.container_last_element, -1) + return element +} + +container_end :: proc() { + container_index := pop(&ctx.containers) + container := &ctx.elements[container_index] + pop(&ctx.container_last_element) + + _, width_is_auto := container.dimensions.width.(Size_Auto) + _, height_is_auto := container.dimensions.height.(Size_Auto) + if width_is_auto || height_is_auto { + br := v2{min(f32), min(f32)} + for element in ctx.elements[container_index:] { + element_br := rect_get_br(get_element_outer_area(element)) + br = {max(br.x, element_br.x), max(br.y, element_br.y)} + } + if width_is_auto { + container.area.width = br.x - container.area.x + } + if height_is_auto { + container.area.height = br.y - container.area.y + } + } + ctx.same_line = false +} + +assert_no_dimensions_percentage_in_auto_container :: proc(dimensions: Dimensions) { + container := _container_current() + if _, is_auto := container.dimensions.width.(Size_Auto); is_auto { + _, is_pixels := dimensions.width.(Size_Pixels) + assert(is_pixels, "Any width that is not Size_Pixels is disallowed in a container using Size_Auto for width") + } + if _, is_auto := container.dimensions.height.(Size_Auto); is_auto { + _, is_pixels := dimensions.height.(Size_Pixels) + assert(is_pixels, "Any height that is not Size_Pixels is disallowed in a container using Size_Auto for height") + } +} + +button :: proc(dimensions: Dimensions, config: ElementConfiguration) -> ^Element { + assert_no_dimensions_percentage_in_auto_container(dimensions) + area := _resolve_element_area(dimensions, config.margin) + return _append_element({ + area = area, + config = config, + type = .Button, + dimensions = dimensions}) +} + +slider :: proc(dimensions: Dimensions, config: ElementConfiguration, data: ElementData_FloatInput) -> ^Element { + assert_no_dimensions_percentage_in_auto_container(dimensions) + area := _resolve_element_area(dimensions, config.margin) + return _append_element({ + area = area, + config = config, + type = .Slider, + data = {float_input_data = data}, + dimensions = dimensions}) +} + +input_float :: proc(dimensions: Dimensions, config: ElementConfiguration, data: ElementData_FloatInput) -> ^Element { + assert_no_dimensions_percentage_in_auto_container(dimensions) + area := _resolve_element_area(dimensions, config.margin) + return _append_element({ + area = area, + config = config, + type = .InputFloat, + data = {float_input_data = data}, + dimensions = dimensions}) +} + +get_element_outer_area :: proc(element: Element) -> rect2 { + return rect_grow(element.area, element.margin * 2.0) +} + +get_element_inner_area :: proc(element: Element) -> rect2 { + return rect_grow(element.area, -element.padding * 2.0) +} + +_append_element :: proc(element: Element) -> ^Element { + append(&ctx.elements, element) + ctx.container_last_element[len(ctx.containers) - 1] = len(ctx.elements) - 1 + return &ctx.elements[len(ctx.elements) - 1] +} + +_resolve_element_area :: proc(dimensions: Dimensions, margin: v2) -> rect2 { + outer_position := _resolve_element_outer_position() + outer_size := _resolve_element_outer_size(dimensions) + return rect_grow(rect2{**outer_position, **outer_size}, -margin * 2.0) +} + +_resolve_element_outer_position :: proc() -> v2 { + defer {ctx.same_line = false} + if alignment_element := _element_container_previous(); alignment_element != nil { + if ctx.same_line { + return rect_get_tr(get_element_outer_area(alignment_element^)) + } else { + return { + get_element_inner_area(_container_current()^).x, + rect_get_bl(get_element_outer_area(alignment_element^)).y + } + } + } + return rect_get_tl(get_element_inner_area(_element_previous()^)) +} + +_resolve_element_outer_size :: proc(dimensions: Dimensions) -> v2 { + return {_resolve_elemment_outer_size_axis(dimensions.width, .X), _resolve_elemment_outer_size_axis(dimensions.height, .Y)} +} + +_resolve_elemment_outer_size_axis :: proc(size: Size, axis: Axis) -> f32 { + switch value in size { + case Size_Pixels: + return cast(f32)value + case Size_Percentage: + inner := get_element_inner_area(_container_current()^) + container_size := inner.width if axis == .X else inner.height + return container_size * f32(value) + case Size_PercentageRemainder: + if element := _element_container_previous(); element != nil { + container_inner := get_element_inner_area(_container_current()^) + last_element_outer := get_element_outer_area(element^) + remaining_container_size: f32 + switch axis { + case .X: remaining_container_size = container_inner.width - rect_get_tr(last_element_outer).x + case .Y: remaining_container_size = container_inner.height - rect_get_bl(last_element_outer).y + } + return remaining_container_size * f32(value) + } else { + inner := get_element_inner_area(_container_current()^) + container_size := inner.width if axis == .X else inner.height + return container_size * f32(value) + } + case Size_Auto: + return 0.0 + } + unreachable() +} + +_container_current :: proc() -> ^Element { + return &ctx.elements[ctx.containers[len(ctx.containers) - 1]] +} + +_element_previous :: proc() -> ^Element { + return &ctx.elements[len(ctx.elements) - 1] +} + +_element_container_previous :: proc() -> ^Element { + if previous_index := ctx.container_last_element[len(ctx.containers) - 1]; previous_index != -1 { + return &ctx.elements[previous_index] + } else { + return nil + } +}