Is Odin Just a More Boring C?
Why I Tried Odin
Background
My recent posts have been diving deep into Zig and C, a shift from my earlier focus on React and JavaScript. This isn’t a pivot but a return to my roots. I started programming at 13 with C and C++, and over the years, I’ve built a wide range of projects in systems programming languages like C, C++, Rust, and now Zig. From hobby experiments and custom Linux utilities to professional embedded systems work think vehicle infotainment, tracking solutions, and low-level components I’ve always been drawn to the power and precision of systems programming. Alongside this, I’ve crafted tools for my own environment and tackled plenty of backend engineering, blending my full-stack expertise with a passion for low-level control.
Why Odin Caught My Eye
I like many others initially dismissed Odin as that language that was primarily intended for game development. It took me a moment or should I say many moments to realize just how stupid that notion was. Because let’s analyze what game development actually means, it means building complex systems that need to be efficient, performant and reliable. It means working with graphics, physics, input handling, networking and more. It means dealing with concurrency, memory management and low level optimizations. In other words, game development is a perfect fit for a systems programming language like Odin.
So basically if it’s intended for game development, then it should be a great fit for general systems programming, desktop applications and since game dev usually means manual memory management without a garbage collector, it should also be possible to some extent to use it for embedded systems.
So after I’ve gave myself a good slap on the forehead for being a bit of an idiot. I decided why not give Odin a fair shot and build something useful with it.
The Project
Now I may have been a bit liberal with the word useful there, what I actually decided to build was something that I usually like to build whenever I wanna try out a new language, namely a tiny key-value store with a pub/sub system. It won’t win any awards for originality and I’m pretty sure the folks over at redis aren’t exactly shaking in their boots. It is the most basic most barebones implementation of both lacking any real useful features that would make it usable in a production environment. But it is a good exercise in understanding the language and its capabilities.
Mainly because it involves a few different aspects of programming that are relevant to systems programming. It involves data structures, memory management, concurrency and networking.
And even if you create something as basic and lacking as I have in this example, you still have room for experimentation and exploration to add more features.
Building a Tiny KV Store With Pub/Sub
My initial minimal proof of concept was simple and straightforward.
package main
import "core:fmt"
import "core:time"
KVStore :: struct {
store: map[string]string,
}
kvstore_init :: proc() -> KVStore {
return KVStore{store = map[string]string{}}
}
kv_put :: proc(kv: ^KVStore, key: string, value: string) {
kv.store[key] = value
}
kv_get :: proc(kv: ^KVStore, key: string) -> string {
if value, ok := kv.store[key]; ok {
return value
}
return ""
}
PubSub :: struct {
subscribers: map[string][]proc(msg: string),
}
pubsub_init :: proc() -> PubSub {
return PubSub{subscribers = map[string][]proc(msg: string){}}
}
subscribe :: proc(ps: ^PubSub, topic: string, handler: proc(msg: string)) {
if arr, ok := ps.subscribers[topic]; ok {
new_arr := make([]proc(msg: string), len(arr)+1);
for i in 0..<len(arr) {
new_arr[i] = arr[i];
}
new_arr[len(arr)] = handler;
ps.subscribers[topic] = new_arr;
} else {
ps.subscribers[topic] = []proc(msg: string){handler};
}
}
publish :: proc(ps: ^PubSub, topic: string, msg: string) {
if handlers, ok := ps.subscribers[topic]; ok {
for handler in handlers {
handler(msg);
}
}
}
kv: KVStore;
main :: proc() {
kv = kvstore_init();
ps := pubsub_init();
handler1 :: proc(msg: string) {
fmt.println("Sub1 got:", msg);
kv_put(&kv, "last_msg", msg);
}
handler2 :: proc(msg: string) {
fmt.println("Sub2 got:", msg);
}
handler3 :: proc(msg: string) {
fmt.println("Sub3 got:", msg);
}
subscribe(&ps, "demo", handler1);
subscribe(&ps, "demo", handler2);
subscribe(&ps, "demo", handler3);
publish(&ps, "demo", "Welcome to dayvster.com");
time.sleep(2 * time.Second);
publish(&ps, "demo", "Here's another message after 2 seconds");
last := kv_get(&kv, "last_msg");
fmt.println("Last in kvstore:", last);
}
As you can see it currently lacks any real error handling, concurrency and persistence. But it does demonstrate the basic functionality of a key-value store with pub/sub capabilities. What I have done is created two main structures, KVStore
and PubSub
. The KVStore
structure contains a map to store key-value pairs and provides functions to put and get values. The PubSub
structure contains a map of subscribers for different topics and provides functions to subscribe to topics and publish messages.
The main
function initializes the key-value store and pub/sub system, defines a few handlers for incoming messages, subscribes them to a topic, and then publishes some messages to demonstrate the functionality.
From this basic example we’ve explored how to handle memory management in Odin, how to work with data structures like maps and slices, and how to define and use procedures.
Memory Management
Like C and Zig, Odin employs manual memory management, but it offers user-friendly utilities to streamline the process, much like Zig, in contrast to C’s more rudimentary approach. For instance, the make
function in Odin enables the creation of slices with a defined length and capacity, akin to Zig’s slice allocation. In the code above, make([]proc(msg: string), len(arr)+1)
generates a slice of procedure pointers with a length of len(arr)+1
. Essentially, it allocates memory on the heap and returns a slice header, which includes a pointer to the allocated memory, along with the length and capacity of the slice.
but how and when is that memory freed? In this code, memory allocated by make
(e.g., for the slice in subscribe
) and for maps (e.g., kv.store
and ps.subscribers
) is not explicitly freed. Since this is a short-lived program, the memory is reclaimed by the operating system when the program exits. However, in a long-running application, you’d need to use Odin’s delete procedure to free slices and maps explicitly. For example:
kvstore_deinit :: proc(kv: ^KVStore) {
delete(kv.store);
}
pubsub_deinit :: proc(ps: ^PubSub) {
for topic, handlers in ps.subscribers {
delete(handlers);
}
delete(ps.subscribers);
}
So let’s add that in the main
function before it exits to ensure we clean up properly:
// ... existing code ...
main :: proc() {
// ... existing code ...
pubsub_deinit(&ps);
kvstore_deinit(&kv);
} // end of main
Well would you look at that, we just added proper memory management to our tiny KV store with pub/sub system and all it took was a few lines of code. I’m still a huge fan of C but this does feel nice and clean, not to mention really readable and easy to understand.
Is our code now perfect and fully memory safe? Not quite, it still needs error handling and thread safety(way later) for production use, but it’s a solid step toward responsible memory management.
Adding concurrency
Enhancing Pub/Sub with Concurrency in Odin
To make our pub/sub system more realistic, we’ve introduced concurrency to the publish procedure using Odin’s core:thread library. This allows subscribers to process messages simultaneously, mimicking real-world pub/sub behavior. Since handler1 modifies kv.store via kv_put, we’ve added a mutex to KVStore to ensure thread-safe access to the shared map. Here’s how it works:
-
Concurrent Execution with Threads: The publish procedure now runs each handler in a separate thread created with thread.create. Each thread receives the handler and message via t.user_args, and thread.start kicks off execution. Threads are collected in a dynamic array (threads), which is cleaned up using defer delete(threads). The thread.join call ensures the program waits for all threads to finish, and thread.destroy frees thread resources. This setup enables handler1, handler2, and handler3 to process messages concurrently, with output order varying based on thread scheduling.
-
Thread Safety with Mutex: Since handler1 updates kv.store via kv_put, concurrent access could lead to race conditions, as Odin’s maps aren’t inherently thread-safe. To address this, a sync.Mutex is added to KVStore. The kv_put and kv_get procedures lock the mutex during map access, ensuring only one thread modifies or reads kv.store at a time. The mutex is initialized in kvstore_init and destroyed in kvstore_deinit.
publish :: proc(ps: ^PubSub, topic: string, msg: string) {
if handlers, ok := ps.subscribers[topic]; ok {
threads := make([dynamic]^thread.Thread, 0, len(handlers))
defer delete(threads)
// Allocate ThreadArgs for each handler
thread_args := make([dynamic]^ThreadArgs, 0, len(handlers))
defer {
for args in thread_args {
free(args)
}
delete(thread_args)
}
for handler in handlers {
msg_ptr := new(string)
msg_ptr^ = msg
t := thread.create(proc(t: ^thread.Thread) {
handler := cast(proc(msg: string)) t.user_args[0]
msg_ptr := cast(^string) t.user_args[1]
handler(msg_ptr^)
free(msg_ptr)
})
t.user_args[0] = rawptr(handler)
t.user_args[1] = rawptr(msg_ptr)
thread.start(t)
append(&threads, t)
}
for t in threads {
thread.join(t)
thread.destroy(t)
}
}
}
This implementation adds concurrency by running each handler in its own thread, allowing parallel message processing. The mutex ensures thread safety for kv.store updates in handler1, preventing race conditions. Odin’s core:thread library simplifies thread management, offering a clean, pthread-like experience.
Odin’s threading feels like a bit like C’s pthreads but without the usual headache, and it’s honestly a breeze to read and write. For this demo, the mutex version keeps everything nice and tidy, However in a real application, you’d still want to consider more robust error handling and possibly a thread pool for efficiency and also some way to handle thread lifecycle and errors and so on…
Adding Persistence
I haven’t added persistence to this code-block personally because I feel that would quickly spiral the demo that I wanted to keep simple and focused into something much more complex. But if you wanted to add persistence, you could use Odin’s core:file
library to read and write the kv.store
map to a file. You would need to serialize the map to a string format (like JSON
or CSV
) when saving and deserialize it when loading. Luckily odin has core:encoding/json
and core:encoding/csv
libraries that can help with this. Which should at the very least make that step fairly trivial.
So if you feel like it, give it a shot and let me know how it goes. Do note that this step is a lot harder than it may seem especially if you want to do it properly and performantly.
Now to Compile and Run
Now here’s the thing the first time I ran odin build .
I thought I messed up somewhere because, it basically took a split second and produced no output no warnings no nothing. But I did see that a binary was produced named after the folder I was in. So I ran it with
❯ ./kvpub
Sub1 got: Welcome to dayvster.com
Sub2 got: Welcome to dayvster.com
Sub3 got: Welcome to dayvster.com
Sub1 got: Here's another message after 2 seconds
Sub2 got: Here's another message after 2 seconds
Sub3 got: Here's another message after 2 seconds
Last in kvstore: Here's another message after 2 seconds
And there you have it, a tiny key-value store with pub/sub capabilities built in Odin. That compiled bizarrely fast, in fact I used a util (pulse) I wrote to benchmark processes and their execution time and it clocked in at a blazing 0.4 seconds to compile
❯ pulse --benchmark --cmd 'odin build .' --runs 3
┌──────────────┬──────┬─────────┬─────────┬─────────┬───────────┬────────────┐
│ Command ┆ Runs ┆ Avg (s) ┆ Min (s) ┆ Max (s) ┆ Max CPU% ┆ Max RAM MB │
╞══════════════╪══════╪═════════╪═════════╪═════════╪═══════════╪════════════╡
│ odin build . ┆ 3 ┆ 0.401 ┆ 0.401 ┆ 0.401 ┆ 0.00 ┆ 0.00 │
└──────────────┴──────┴─────────┴─────────┴─────────┴───────────┴────────────┘
Well I couldn’t believe that so I ran it again this time with --runs 16
to get a better average and it still came in at a very respectable 0.45
(MAX) seconds.
OK that is pretty impressive. but consistent maybe my tool is broken? I’m not infallible after all. So I re-confirmed it why hyperfine
and it came out at:
❯ hyperfine "odin build ."
Benchmark 1: odin build .
Time (mean ± σ): 385.1 ms ± 12.5 ms [User: 847.1 ms, System: 354.6 ms]
Range (min … max): 357.3 ms … 400.1 ms 10 runs
God damn that is fast, now I know the program is tiny and simple but still that is impressive and makes me wonder how it would handle a larger codebase. Please if you have any feedback or insights on this let me know I am really curious.
just for sanitysake I also ran time odin build .
and it came out at you’ve guessed it 0.4
seconds.
Right so it’s fast, but how’s the experience?
Well I have to say it was pretty smooth overall. The compiler is fast and the error messages are generally clear and helpful if not perhaps a bit… verbose for my taste
For example I’ve intentionally introduced a simple typo in the map
keyword and named is masp
to showcase what I mean:
❯ odin build .
/home/dave/Workspace/TMP/odinest/main.odin(44:31) Error: Expected an operand, got ]
subscribers: masp[string][]proc(msg: string),
^
/home/dave/Workspace/TMP/odinest/main.odin(44:32) Syntax Error: Expected '}', got 'proc'
subscribers: masp[string][]proc(msg: string),
^
/home/dave/Workspace/TMP/odinest/main.odin(44:40) Syntax Error: Expected ')', got ':'
subscribers: masp[string][]proc(msg: string),
^
/home/dave/Workspace/TMP/odinest/main.odin(44:41) Syntax Error: Expected ';', got identifier
subscribers: masp[string][]proc(msg: string),
^
I chose specifically this map because I wanted to showcase how Odin handles errors when you try to build, it could simply say Error: Unknown type 'masp'
but instead it goes on to produce 4 separate errors that all stem from the same root cause. This is obviously because the parser gets confused and can’t make sense of the code anymore. So essentially you get every single error that results from the initial mistake even if they are on the same line. Now would I love to see them condensed into a single error message? Because it stems from the same line and the same root cause? Yes I would. But that’s just my personal preference.
Where Odin Shines
Simplicity and Readability
Odin kinda feels like a modernized somehow even more boring C but in the best way possible. It’s simple, straightforward and easy to read. It does not try to have some sort of clever syntax or fancy features, it really feels like a no-nonsense no frills language that wants you to start coding and being productive as quickly as possible.
In fact this brings me to my next point.
The Built in Libraries Galore
I was frankly blown away with just how much is included in the standard and vendored(more on that later) libraries. I mean it has everything you’d expect from a modern systems programming language but it also comes with a ton of complete data structures, algorithms and utilities that you would usually have to rely on third-party libraries for in C or even Zig.
For more info just look at Odin’s Core Library and I mean really look at it and read it do not just skim it. Here’s an example flags which is a complete command line argument parser, or even rbtree which is a complete implementation of a red-black tree data structure that you can just import and use right away
But what really blew me away was
The Built in Vendor Libraries / Packages
Odin comes with a set of vendor libraries that basically give you useful bindings to stuff like SDL2/3
, OpenGL
, Vulkan
, Raylib
, DirectX
and more. This is really impressive because it means you can start building games or graphics applications right away without having to worry about setting up bindings or dealing with C interop. Now I’m not super sure if these vendor bindings are all maintained and created by the Odin team from what I could gather so far, it would certainly seem so but I could be wrong. If you know more about this please let me know.
But all that aside these bindings are really well done and easy to use. For example here’s how you can create a simple window with SDL2 in Odin:
package main
import sdl "vendor:sdl2"
main :: proc() {
sdl.Init(sdl.INIT_VIDEO)
defer sdl.Quit()
window := sdl.CreateWindow(
"Odin SDL2 Black Window",
sdl.WINDOWPOS_CENTERED,
sdl.WINDOWPOS_CENTERED,
800,
600,
sdl.WINDOW_SHOWN,
)
defer sdl.DestroyWindow(window)
renderer := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED)
defer sdl.DestroyRenderer(renderer)
event: sdl.Event
running := true
for running {
for sdl.PollEvent(&event) {
if event.type == sdl.EventType.QUIT {
running = false
}
}
sdl.SetRenderDrawColor(renderer, 0, 0, 0, 255)
sdl.RenderClear(renderer)
sdl.RenderPresent(renderer)
}
}
This code creates a simple window with a black background using SDL2. It’s pretty straightforward and easy to understand, especially if you’re already familiar with SDL2 or SDL3.
C Interop
Odin makes it trivially easy to interop with C libraries, as long as that. This is done via their foreign import
where you’d create an import name and link to the library file and foreign
blocks to link to declared individual function or types.
I could explain it with examples here but Odin’s own documentation does a way better job and will keep this post from getting even longer than it already is. So please check out Odin’s C interop documentation for more info.
Where Odin Feels Awkward
Standard Library Gaps
While Odin’s standard library is quite comprehensive, there are still some gaps and missing features that can make certain tasks more cumbersome. For example, while it has basic file I/O capabilities, it lacks more advanced features like file watching or asynchronous I/O. Additionally, while it has a decent set of data structures, it lacks some more specialized ones like tries or bloom filters I’d also love to see a b+ tree implementation in the core library.
But those are at most nitpicks and finding third-party libraries or writing your own implementations is usually straightforward.
However…
No Package Manager
I really like languages that come with their own package manager, it makes it so much easier to discover, install and manage third-party libraries / dependencies. Odin currently lacks a built-in package manager, which means you have to manually download and include third-party libraries in your projects. This can be a bit of a hassle, especially I’d imagine for larger projects with multiple dependencies.
Smaller Nitpicks
-
dir inconsistencies: I love how it auto named my binary after the folder I was in but I wish it did the same whenever I ran
odin run
andodin build
I had to explicitly specifyodin run .
andodin build .
that felt a bit inconsistent to me because if it knows the folder we are in why not just use that as the default value when we wanna tell it to run or build in the current directory? -
Error messages: As mentioned earlier, while Odin’s error messages are generally clear, they can sometimes be overly verbose, especially when multiple errors stem from a single root cause. It would be nice to see more concise error reporting in such cases.
So to fix this I’d love to either see error messages collapsed into a single message with an array of messages from the same line, or somehow grouped together into blocks.
Pointers are ^ and not *
I’m on a German keyboard and the ^
character is a bit of a pain to type, especially when compared to the *
character which is right next to the Enter
key on my keyboard. I get that Odin wants to differentiate itself from C and C++ but this small change feels unnecessary and adds a bit of friction to the coding experience.
These are as the title says just minor nitpicks and in no way detract from the overall experience of using Odin, just minor annoyances that I personally had while using the language your experience may differ vastly and none of these may even bother you.
So is Odin just a More Boring C?
In a way, yes kind of. I mean it’s very similar in approach and philosophy but with more “guard rails” and helpful utilities to make the experience smoother and more enjoyable and the what I so far assume are first party bindings to popular libraries via the vendors package really makes it stand out in a great way, where you get a lot more consistency and predictability than you would if you were to use C with those same libraries.
And I guess that’s the strength of Odin, it’s so boring that it just let’s you be a productive programmer without getting in your way or trying to be too clever or fancy.
I use boring here in an affectionate way, if you’ve ever read any of my other posts you’ll know that I do not appreciate complexity and unnecessary cleverness in programming which is why I suppose I’m often quite critical of rust even though I do like it for certain use cases.
In this case I’d say Odin is very similar to Go both are fantastic boring languages that let you get stuff done without much fuss or hassle. The only difference is that Go decided to ship with a garbage collector and Odin did not, which honestly for me personally makes Odin vastly more appealing.
Syntax and Ergonomics
Odin’s syntax is like C with a modern makeover clean, readable, and less prone to boilerplate. It did take me quite a while to get used to replacing my muscle memory for *
with ^
for pointers and func
, fn
, fun
, function
with proc
for functions. But once I got over that initial hump, it felt pretty natural.
Also ::
for type declarations is a bit unusual and took me longer than I care to admit, as I’m fairly used to ::
being used for scope resolution in languages like C++ and Rust. But again, once I got used to it, it felt fine.
Everything else about the syntax felt pretty intuitive and straightforward.
Who Odin Might Be Right For
Ideal Use Cases
-
Game Development: Honestly I totally see where people are coming from when they say Odin is great for game development. The built-in vendor libraries for SDL2/3, OpenGL, Vulkan, Raylib and more make it super easy to get started with game development. Plus the language’s performance and low-level capabilities are a great fit for the demands of game programming.
-
Systems Programming: Odin’s manual memory management, low-level access, and performance make it a solid choice for systems programming tasks like writing operating systems, device drivers, or embedded systems. I will absolutely be writing some utilities for my Linux setup in Odin in the near future.
-
Desktop Applications: Again this is where those vendor libraries shine, making it easy to build cross-platform desktop applications with graphical interfaces as long as you’re fine with doing some manual drawing of components, I’d love to see a binding for something like
GTK
orQt
in the vendor packages in the future. -
General Purpose Programming: This brings me back to my intro where I said that it took me a while to realize that if Odin is good for game development then realistically by all means it should basically be good for anything and everything you wish to create with it. So yea give it a shot make something cool with it.
Where It’s Not a Good Fit Yet
- Web Development: The Net library is pretty darn nice and very extensive, however it does seem like it’s maybe a bit more fit for general networking tasks rather than simplifying your life as a web backend developer. I’m sure there’s already a bunch of third party libraries for this, but if you’re a web dev you are almost spoiled for choice at the moment by languages that support web development out of the box with all the fixings and doodads.
Final Thoughts
Would I Use It Again?
Absolutely in fact I will, I’ve already started planning some small utilities for my Linux setup in Odin. I really like the simplicity and readability of the language, as well as the comprehensive standard and vendor libraries. The performance is also impressive, especially the fast compile times.
Source Code and Feedback
You can find the complete source code for the tiny key-value store with pub/sub capabilities on my GitHub: dayvster/odin-kvpub
If you create anything cool with it I’d love to see it so do hit me up on any of my socials.
I’d love to hear your thoughts and experiences with Odin, whether you’ve used it before or are considering giving it a try. Feel free to leave a comment or reach out to me on Twitter @dayvster.
Appreciate the time you took to read this post, and happy coding!