Karl Zylinski

A programming language made for me

May 12, 2025 Table of Contents

在我的书 Understanding the Odin Programming Language 中,我写道“Odin 将我最喜欢的 C 语言最佳实践直接融入到了语言中”。但我并没有详细说明。 让我们在这里详细说明一下! 这让我想起了之前的一份工作。 2021 年,我在一家名为 Our Machinery 的公司工作。 我们用纯 C 语言创建了一个完整的游戏引擎。 我们用一种非常舒适且强大的方式来编写 C 代码。 我们依赖于以下概念:

在那里工作时,我偶然发现了 Odin。 我读了一些关于它的内容。 它似乎包含了所有这些东西。 在许多方面,它看起来像是围绕着我们在工作中编写 C 代码的特定方式而构建的语言。 由于我喜欢这种编程方式,所以它几乎就像是为我量身定制的语言!

Custom allocators#

在我的工作中,我们在 C 语言中实现了我们自己的 Allocator 接口。 Allocator 提供了一种自定义的方式来执行动态内存分配。 C 程序员习惯于 mallocfree。 但是你可以创建提供更高级分配策略的 allocators。 我们的 Allocator 接口使我们能够以统一的方式推理 allocators 并将它们传递给函数。 如果一个函数接受 Allocator 类型的参数,那么它暗示它的返回值是动态分配的。 这正是 Odin 中的工作方式。 但是 Allocator 接口 内置于语言的 base 库集合中! 这意味着 Odin 的 basecore 库也支持这些 allocators。 在我的工作中,Allocator 接口仅在我们的代码中受支持:C 标准库不支持任何这些功能。 但是在 Odin 中,我自己的代码 core 库都可以推理 custom allocators,从而使这个概念更加强大。

在 Odin 中,corebase 是编译器附带的两个库集合。 有些人将它们称为 Odin 的“标准库”。 但是它们作为源代码与编译器一起提供。 我们鼓励您制作 core 中 packages 的副本,以便根据自己的需求定制这些 packages。 因此,它更像是一个“默认库”而不是“标准库”。 有一个合理的默认设置,但您可以随意执行任何操作。

Temporary allocators#

Temporary memory allocators 提供了一种执行动态内存分配的方式,这些分配仅在短时间内需要。 什么是“短时间”? 视频游戏有一个非常方便的“短时间”:一帧。 在我的 C 语言工作中,我有一个临时 allocator,我经常使用它。 不再需要手动 mallocfree 仅在短时间内需要的字符串和数组。 只需使用 temp allocator 即可。 它将在下一帧消失! 而且它更有效率:temp allocator 分配到预先分配的内存块中。 我很高兴发现 Odin 具有完全相同的功能。 有一个内置的 temp allocator,其名称为 context.temp_allocator。 同样,core 库和我的代码使用相同的 Allocator 接口。 因此,我可以将 context.temp_allocator 传递给接受 Allocator 参数的任何 core 库过程。 该过程分配的任何内容都将是临时的。 赞!

Odin 允许您选择何时清除 temp allocator。 您可以通过将 free_all(context.temp_allocator) 放在代码中的某个位置来做到这一点。 在视频游戏中,我会将其作为“主游戏循环”的最后一行。

Tracking allocators#

手动内存管理可能看起来很难。 你怎么知道你是否在泄漏内存? 在我的 C 语言工作中,我们有一个特殊的 tracking allocator,它可以包装任何其他 allocator。 它记录了何时发生分配,并记录了何时释放分配。 这样,如果没有任何内容被释放,我们可以在关闭时显示警告。 这正是 Odin 附带的 tracking allocator 的工作方式。 只需将下面的代码放在你的 main 过程的顶部。 它将在关闭时打印内存泄漏列表。

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

copy

你还需要在文件的顶部执行 import "core:mem"import "core:fmt"。 上面的代码来自 Odin overview

Zero is initialized (ZII)#

ZII,是 zero is initialized 的缩写,意味着你尝试使内存的零值在尽可能多的情况下有效。 在 Odin 中,所有变量都会自动进行零初始化。 不仅是整数和浮点数。 还有所有 structs。 当创建这些变量时,它们的内存会被零填充。 因此,如果 Some_Type 是一个 struct,那么你可以简单地编写以下行来声明并零初始化该类型的变量:

x: Some_Type

copy 这使得 ZII 更加强大! 变量意外未初始化的风险很小。 你可以依靠零初始化。 此外,整个 Odin 的 core 库也依赖于 ZII。 因此,在整个语言及其生态系统中,它都感觉非常自然。

你可以通过编写 x: Some_Type = --- 来跳过零初始化。 你很少需要这样做,但在某些特定的、对性能敏感的情况下,这可能是一个好主意。 零初始化是 opt out 而不是 opt in,这很棒。 这样,由于未初始化的内存,我们得到的 bug 就会少得多。

Designated initializers#

这是一个内置于 C 和 Odin 中的功能。 下面的代码将创建一个 My_Type 类型的变量 x。 它会将字段 number 初始化为 7。 任何未提及的字段都将被零初始化。 这与“zero is initialized”概念非常吻合。

My_Type :: struct {
	number: int,
	sub_thing: Another_Type,
}
Another_Type :: struct {
	some: int,
	more: f32,
	state: bool,
}
x := My_Type {
	number = 7,	
}

copy

Cache friendly programming#

CPU 内部有一些非常快的内存。 这被称为缓存。 如果你保持缓存中填充着 CPU 可能需要的任何数据,那么你的程序将运行得非常快。 在我的 C 语言工作中,我们有一个实体组件系统 (ECS),它使用了所谓的“Structure of Arrays”(SoA)。 这是一种内存布局,在某些情况下,可以帮助你的 CPU 缓存填充相关数据。 任何在 C 语言中编写过 SoA 数据类型的人都知道这不是很愉快。 但是,Odin 具有内置的 SoA 支持。 只需在数组声明之前添加 #soa。 它会自动为你重新排列内存布局。 例如,以下代码使用“默认布局”。 也称为“Arrays of Structures”(AoS):

Person :: struct {
	health: int,
	age: int,
}
people: [128]Person

copy people 数组的内存布局如下所示:

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

copy 如果在 [128]Person 前面添加 #soa,如下所示:

Person :: struct {
	health: int,
	age: int,
}
people: #soa[128]Person

copy 那么 people 的内存布局将改为如下所示:

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

copy 在 C 语言中实现这一点是手动工作。 但是在这里,你只需添加 #soa。 现在,不要仅仅因为你能就到处添加 #soa。 它仍然会使代码编写起来有点棘手(你需要使用 #soa 指针等,而不是普通指针)。 代码调试起来也会有点困难。 如果你有明确的性能优势证明,则将其放入。

顺便说一句,我不鼓励任何制作自己的视频游戏的人制作 ECS。 这通常不是一个好主意。 也许对于一些大型游戏引擎来说,这是一个好主意。 但是对于一个小项目来说,它可能只会使你的代码更难编写,从而使你的游戏更糟。 我觉得那些在开始编写游戏代码之前编写 ECS 的人实际上并不想制作游戏:他们想制作通用的游戏引擎。 这很好,但要确保你没有对自己撒谎关于你想做什么。 如果你想制作一个游戏,那就制作一个游戏。 编写你解决手头问题所需的代码,不要假装自己是一家大型游戏引擎公司。

Finally: Simplicity#

Odin 是一种非常简单的语言。 我的工作选择使用 C 而不是 C++ 的部分原因是 C 语言的简单性。 但是我们有时会错过 C++ 的一些现代思想。 然而,C++ 是一头巨大的野兽。 我们不想打开那个潘多拉魔盒。 Odin 保留了 C 语言的简单性,同时带来了一些不错的现代特性,例如 generics 和(显式)overloading。 但是该语言仍然保持小而简单。 并且它注定要保持这样。 在过去的几年中,Odin 几乎没有添加任何语言功能。 目前,主要是 core 库正在发生重大变化。

Not everyone has my programming background#

由于 Odin 与我编写 C 代码的方式非常相似,因此学习 Odin 对我来说很自然。 你可能来自不同的背景。 如果这些东西对你来说不熟悉,但你仍然想学习 Odin,那么你或许可以从阅读我的书 Understanding the Odin Programming Language 中受益。 它旨在作为该语言的读者友好的介绍。 这本书试图给你我在发现 Odin 时已经有过的见解。

Thanks for reading#

为什么不来我的 Discord 服务器逛逛呢? 在那里你可以问我问题以及讨论 Odin 和游戏开发。 祝你愉快! /Karl Zylinski