I’m interested in using Odin to write robustly safe and performant code which is easy to maintain and test - under adversarial networked conditions. The use-case is one which can’t so easily tolerate large bugs and can’t be as “tested during release” as other applications might be able to tolerate.
I’m weighing formally verified languages against Odin - relevant advantages are: readability, modifiability, performance, relatively sane defaults for safety, and of course simplicity - not to mention the ability to FFI with the above benefits to some important dependencies in Rust or C.
I plan to make full use of Odin’s great package system and testing to get all the possible benefits of modularisation, and sum types alongside (mostly) functional procedures to ensure the program has limited and known possible states. Obviously will make liberal use of assert
.
I’m discovering useful features as I go: for example, Distinct Types seem to allow me to ensure values of the same fundamental type cannot be used in unintended ways, similar to Ada’s “Super-Strongly Typed” design.
Must say I lament no ability to do dependent type style constraints (arbitrary restrictions on type values) for this use case, but understand why Odin doesn’t go that route - FWIW Rust doesn’t even appear to offer it.
My biggest concern is memory safety.
My general plan is to use only Arena or Stack allocations, and liberal use of @(private="file")
to isolate effects in modules and prevent & contain disasters.
As someone likely with less experience than many here both in Odin and systems-programming, I’d be grateful to hear about any other tactics Odin lets me leverage to achieve these goals, or even more general advice like “you really should just use XYZ language.”
3 Likes
There are many different ways to do this, depending on your constraints.
-
For example, if you never free any memory at all, you can never have a use-after-free bug. Depending on your use case, you might in fact have a situation where you never use more than some relatively small amount of memory and this is perfectly fine to let the OS handle cleanup at process exit; it’s not a golden rule that all memory must be freed immediately when it is done with.
-
As for bounds-checking, Odin already checks all bounds by default: arrays are checked at compile-time and slices/dynamic arrays are checked at run-time. If you erase the type information of a slice or array with raw_data
, you will lose this guarantee. Pointers and multi-pointers have no knowledge of range, naturally, as that is the domain of slices.
-
Increased support for AddressSanitizer was merged within the recent past, which will guarantee better detection of bugs in custom memory allocators. The new heap allocator I’m working on makes extensive use of ASan (when enabled) to prevent buffer overflows, even when type information has been erased and the default bounds-checking cannot catch it.
-
Use of @private
won’t lead to any direct decrease in memory bugs; it will only constrain the usage of procedures that may introduce those bugs.
-
In a situation where you may run out of memory but don’t want to panic or raise an error, there’s the idea of a global, blank slate return value. It only works in allocators that return specific types, but if you hit a lack of memory, the solution is to return a space that was set aside at init for just this occasion. That space will be overwritten and may result in overlapping/garbage data, but at least the users of the memory will have something to work with and will not crash for lack of memory. I first heard of this idea from Casey Muratori in the context of an entity system for games.
In the end, there’s no magic solution to memory safety once you start working with pointer arithmetic. Even if you never free any memory at all, you have to make sure your calculations are valid or you risk a segfault, or worse: interacting with valid memory that you didn’t intend to.
At this level, you have to know how to handle memory. There’s no way around it.
I think if you want to avoid as many memory bugs as possible, you:
- Never do any pointer arithmetic, and
- Never free any memory until program exit.
Even then you still have to make sure your index calculations in the valid arrays are correct, and depending on how strict you want to be, you might even forbid the usage of arrays and slices to completely cull the possibility of invalid indexing (the infamous off-by-one error). So perhaps you could add a #3 which is to never use indexed data structures. Maybe in place of #2: you don’t even dynamically allocate memory, full stop.
Some of this may sound crazy, but it will definitely limit your capacity for bugs, depending on if you’re writing the next moon lander software or something critical to life.
5 Likes
Thanks for the thoughtful response - I read a bit about your heap allocator while searching for topics and thinking about my post and look forward to it.
- Never free any memory until program exit.
The Null Garbage Collector is a fun insight which I’m sure you knew about. 
Certainly I can imagine modules which will do that - I think often the simplest structure which will get the job done for my needs as an ‘extended’ stack, one that goes beyond a single procedure, and then carefully writing the module so the stack terminates predictably.
I will be doing more research and thinking about this generally as well on suggestions you made. Thanks again.
2 Likes