为我定制的编程语言
Odin: A programming language made for me

原始链接: https://zylinski.se/posts/a-programming-language-for-me/

Odin 融入了多项 C 语言最佳实践,这源于作者在 Our Machinery 的经验,在那里他们使用 C 语言开发了一个游戏引擎。Odin 内置了自定义内存分配器,允许使用超越 `malloc` 和 `free` 的高级内存分配策略,这与他们自定义的 C 语言内存分配器接口如出一辙。临时的内存分配器对于游戏开发至关重要,可以通过 `context.temp_allocator` 方便地访问,简化了短生命周期动态内存的管理。追踪分配器有助于检测内存泄漏,在程序关闭时打印未释放的内存分配。Odin 强调“零初始化”(ZII),自动将变量、结构体和其他数据结构清零初始化。类似于 C 语言的结构体指定初始化器允许选择性地初始化字段。最后,Odin 通过 `#soa` 指令内置支持数组结构体 (SoA),提高了缓存友好性。Odin 优先考虑简洁性,保留了 C 语言的优雅性,同时融入了泛型等现代特性。

Hacker News 的讨论线程围绕 Odin 编程语言展开,重点关注其设计选择,特别是自动零初始化 (ZII)。一些用户批评 ZII 会掩盖错误,因为错误的零值会被传播,建议改为在编译时报错未写入内存的读取。另一些用户则认为 ZII 可重复且方便,并指出其潜在的性能优势以及对某些编程风格(尤其是在游戏开发中)的适用性。 讨论延伸到 C 等语言中未初始化变量的更广泛问题,显式初始化的优点,以及严格的安全措施和易用性之间的权衡。还讨论了为 C 创建新的标准库,以及这是否值得。人们对安全性和性能之间的权衡以及远离原始字节的高级抽象数据类型表示担忧。Odin 的创建者 gingerBill 解释了 Odin 设计背后的基本原理以及尝试“修复”C 时遇到的局限性。

原文

In my book Understanding the Odin Programming Language I wrote that “Odin incorporates some of my favorite C best practices, straight into the language”. But I didn’t really elaborate on the details. Let’s do that here!

This brings me to talking a bit about a previous job I had. Back in 2021 I worked at a place called Our Machinery. We were creating a whole game engine in plain C. We used a very comfortable and powerful way to program C.

We relied on concepts such as:

  • Custom allocators
  • Temporary allocators
  • Tracking allocators
  • Designated initializers
  • Zero is initialized
  • Cache friendly programming

While working there, I stumbled upon Odin. I read a bit about it. It seemed to incorporate all these things. In many ways it seemed like a language built around the specific way in which we programmed C at my job. Since I liked that way of programming, it almost seemed like a language built for me!

Custom allocators

At my job we had implemented our own Allocator interface in C. An allocator provides a custom way in which one can do dynamic memory allocations. C programmers are used to malloc and free. But you can make allocators that provide more advanced allocation strategies. Our Allocator interface made it possible to reason about allocators in a uniform way and pass them around to functions.

If a function accepted a parameter of type Allocator, then it was a hint that its return value was dynamically allocated.

This is exactly how it works in Odin. But the Allocator interface is built into the language’s base library collection! This meant that Odin’s base and core libraries support these allocators too. At my job, the Allocator interface was only supported in our own code: The C standard library didn’t support any of that. But in Odin, my own code and the core libraries can reason about custom allocators, making the concept even more powerful.

In Odin, core and base are two collections of libraries that come with the compiler. Some refer to them as the “standard library” of Odin. But they are shipped as source with the compiler. You are encouraged to make copies of the packages inside core, making it possible to tailor those packages to your own needs. So it’s more of a “default library” than a “standard library”. There’s a sensible default, but you’re free do things however you want.

Temporary allocators

Temporary memory allocators provide a way to do dynamic memory allocations that are only needed for a short while. What’s “a short while”? Video games have a very convenient “short while”: A single frame.

At my C job we had a temporary allocator that I used a lot. Gone was the need to manually malloc and free strings and arrays that were only needed for a short while. Just use the temp allocator. It’ll be gone the next frame! And it’s more efficient: The temp allocator allocates into pre-allocated blocks of memory.

I was happy to discover that Odin came with exactly this functionality. There’s a built in temp allocator available under the name context.temp_allocator. And again, the core libraries and my code uses the same Allocator interface. So I can just pass context.temp_allocator into any core library procedure that accepts an Allocator parameter. Whatever that procedure allocates will then be temporary. Nice!

Odin lets you choose when to clear the temp allocator. You do that by putting free_all(context.temp_allocator) somewhere in your code. In a video game, I’d put it as the last line of the “main game loop”.

Tracking allocators

Manual memory management may seem hard. How do you know if you’re leaking memory?

At my C job we had a special tracking allocator that could wrap any other allocator. It recorded when an allocation happened, and recorded when it was deallocated. That way we could display a warning on shutdown, if anything hadn’t been deallocated.

This is exactly how the tracking allocator that comes with Odin works. Just plop the code below at the top of your main procedure. It’ll print a list of memory leaks on shutdown.

track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)

defer {
	if len(track.allocation_map) > 0 {
		fmt.eprintf("=== %v allocations not freed: ===\n", len(track.allocation_map))
		for _, entry in track.allocation_map {
			fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
		}
	}
	mem.tracking_allocator_destroy(&track)
}

You’ll also need to do import "core:mem" and import "core:fmt" at the top of the file. The code above is from the Odin overview.

Zero is initialized (ZII)

ZII, short for zero is initialized, means that you try to make the zero-value of memory valid in as many situations as possible.

In Odin all variables are automatically zero initialized. Not just integers and floats. But all structs as well. Their memory is filled with zeroes when those variables are created. So if Some_Type is a struct, then you can just write the following line to declare and zero-initialize a variable of that type:

This makes ZII extra powerful! There is little risk of variables accidentally being uninitialized. You can lean on that zero initialization. Also, the whole core library of Odin relies on ZII as well. So it feels very natural throughout the whole language and its ecosystem.

You can skip zero initialization by writing x: Some_Type = ---. You rarely need to do so, but it can be a good idea in some specific, performance sensitive situations. It’s great that zero initialization is opt out, instead of opt in. That way we get way less bugs due to uninitialized memory.

Designated initializers

This is a feature built into both C and Odin. The code below will create a variable x of type My_Type. It’ll initialize the field number to 7. Any non-mentioned field will be zero-initialized. This plays very well together with the “zero is initialized” concept.

My_Type :: struct {
	number: int,
	sub_thing: Another_Type,
}

Another_Type :: struct {
	some: int,
	more: f32,
	state: bool,
}

x := My_Type {
	number = 7,	
}

Cache friendly programming

The CPU has some memory inside it that is very fast. It’s called a cache. If you keep the cache filled with whatever data the CPU might need next, then your program will run very fast.

At my C job we had an entity-component-system (ECS) that used what is known as “Structure of Arrays” (SoA). That’s a memory layout that can, in certain circumstances, help fill your CPU cache with relevant data. Anyone who has written SoA data types in C knows it’s not very fun.

However, Odin comes with built in SoA support. Just put #soa in front of an array declaration. It’ll automatically re-arrange the memory layout for you.

As an example, the following code uses the “default layout”. Also known as “Arrays of Structures” (AoS):

Person :: struct {
	health: int,
	age: int,
}

people: [128]Person

The memory layout of the people array looks like this:

people[0].health
people[0].age
people[1].health
people[1].age
people[2].health
people[2].age
people[3].health
people[3].age
people[4].health
people[4].age
... etc

If you add #soa in front of [128]Person, like so:

Person :: struct {
	health: int,
	age: int,
}

people: #soa[128]Person

then the memory layout of people will instead look like this:

people[0].health
people[1].health
people[2].health
people[3].health
people[4].health
... and 123 more health items
people[0].age
people[1].age
people[2].age
people[3].age
people[4].age
... and 123 more age items

Achieving this in C is manual work. But here you just add #soa.

Now, don’t go putting #soa everywhere, just because you can. It will still make the code a bit trickier to write (you need to use #soa pointers etc instead of normal pointers). The code will also be a bit harder to debug. Put it in if you have proof of a clear performance benefit.

By the way, I discourage anyone who is making their own video game from making an ECS. It’s usually not a good idea. Perhaps it’s a good idea for some massive game engines. But for a small project it might just make your code harder to write and thereby your game worse. I feel like people who write an ECS before starting on their gameplay code are people who actually don’t want to make games: They want to make general purpose game engines. That’s fine, but make sure you’re not lying to yourself about what it is you want to do. If you want to make a game, then just make a game. Write the code that you need for the problem at hand, don’t pretend to be a giant game engine company.

Finally: Simplicity

Odin is very simple language. The reason my job used C over C++ was partially because of the simplicity of C. But we did sometimes miss some modern ideas of C++. However, C++ is a massive beast. We didn’t want to open that can of worms.

Odin retains the simplicity of C while bringing along some nice modern things, such as generics and (explicit) overloading. But the language is still kept small and simple. And it is meant to remain so. Very few language features have been added to Odin over the last few years. It’s mostly the core libraries that are getting major changes at this point.

Not everyone has my programming background

Learning Odin came naturally to me, due to it being so similar to the way in which I wrote C code.

You may come from a different background. If these things are unfamiliar to you, but you still want to learn Odin, then perhaps you’d benefit from reading my book Understanding the Odin Programming Language. It’s meant to be a reader-friendly intro to the language. The book tries to give you the insights I already had when I discovered Odin.

Thanks for reading!

Why not drop by my Discord server? On there you can ask me questions as well as discuss Odin and game development.

Have a nice day!

/Karl Zylinski

联系我们 contact @ memedata.com