fuck submodules part 3

This commit is contained in:
Synthasmagoria 2026-07-30 20:16:09 +02:00
commit 88dee75f7d
26 changed files with 2962 additions and 0 deletions

View file

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

View file

@ -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
```

View file

@ -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)
}
}

View file

@ -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..<tbl.nr_rows {
for col in 0..<tbl.nr_cols {
table.write_table_cell(w, tbl, row, col)
}
io.write_byte(w, '\n')
}
}
// The dbghelp library of win32 is not thread safe, this library uses this mutex to get exclusive access.
// It is provided in case you want to use the dbghelp library, and want to coordinate access with this package.
_win32_dbghelp_mutex: sync.Mutex

View file

@ -0,0 +1,160 @@
#+vet explicit-allocators
#+private file
package back
@require import "base:runtime"
@require import "core:strings"
@require import "core:sys/posix"
when !USE_FALLBACK {
foreign import system "system:System.framework"
// NOTE: CoreSymbolication is a private framework, Apple is allowed to break it and doesn't provide
// headers, although the API has as of my knowledge been the same in the past 10 years at least.
@(extra_linker_flags="-iframework /System/Library/PrivateFrameworks")
foreign import symbolication "system:CoreSymbolication.framework"
@(private="package")
_Trace_Entry :: rawptr
@(private="package")
_trace :: #force_no_inline proc(buf: Trace) -> (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 ---
}
}

View file

@ -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..<count {
out[i].symbol, err = process_line(strings.split_lines_iterator(&sstdout), allocator)
if err == .Out_Of_Memory {
out[i].symbol = "??OOM"
} else if err != nil {
return
}
// TODO: parse location and transform to ODIN_ERROR_POS_STYLE
out[i].location, err = process_line(strings.split_lines_iterator(&sstdout), allocator)
if err == .Out_Of_Memory {
out[i].location = "??OOM"
} else if err != nil {
return
}
if out[i].location == "" || out[i].location == "??" {
fallback, mem_err := strings.clone_from(msgs[i], allocator)
if mem_err != nil { return 0, .Out_Of_Memory }
out[i].location = fallback
}
}
return count, nil
}
process_line :: proc(line: string, ok: bool, allocator: runtime.Allocator) -> (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) ---
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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() {}

View file

@ -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))
})
}

View file

@ -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
})
}

View file

@ -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:
}
}
}

View file

@ -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))
})
}

View file

@ -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() {}