QEMU 编码风格
提交前请用 scripts/checkpatch.pl 检查补丁。
格式与风格
仓库的 .editorconfig 可帮助编辑器采用正确设置, 详情见 https://editorconfig.org/。
空白
任何编码风格最重要的当然都是空白。 老程序员连鼻梁上的眼镜都找不到,却能在十五秒差距之外分辨制表符和八个空格。 无数争论都曾围绕此事展开。
QEMU 使用四个空格缩进,绝不使用制表符, 只有语法已无法挽回地要求制表符的 Makefile 例外。 空格优于制表符,因为:
只有一种表达空白的方式,减少歧义和错误。
不再纠结“制表符缩进、空格对齐”。
制表符把代码推得太靠右,使屏幕严重失衡。
编辑器未正确配置八列制表位时,会显示错误。
补丁中制表符显示不好,几乎每行都可能产生偏一列的问题。
这是 QEMU 编码风格。
不得保留行尾空白。
多行缩进
以下位置需要缩进:
if/else;
while/for;
函数定义和调用。
长行折行后,后续行必须适当缩进。 if/else、while/for 的后续行与首行左括号之后对齐。例如:
if (a == 1 &&
b == 2) {
while (a == 1 &&
b == 2) {
函数有两种形式:
相对行首缩进四格;
与首行左括号之后对齐。
例如:
do_something(x, y,
z);
do_something(x, y,
z);
do_something(x, do_another(y,
z));
行宽
应尽量将行限制在 80 个字符。
遇到函数或符号名很长的子系统时可能难以做到。 若在第 80 列换行明显更难读、更别扭,宁可保持一行; 85 字符的一行优于不自然的折行。
即使如此,也不要远超 80。 checkpatch 在 100 字符报警只是防止明显过长,不是建议目标。
理由:
有人喜欢在 24 英寸屏幕上铺满 6×4 个 80×24 xterm,并全用 vi。 最好的惩罚就是让他们继续这么做。
合理行宽使代码,尤其补丁更易阅读,80 是传统值。
四格缩进让“左边空白太多”的借口不再成立。
这是 QEMU 编码风格。
命名
变量使用 lower_case_with_underscores,便于输入和阅读。 结构化类型采用 CamelCase,虽然难输入,但更醒目。 枚举和函数类型也采用 CamelCase。 标量类型采用以下划线分隔并以 _t 结尾的形式,类似 POSIX uint64_t。 最后这一约定与 POSIX 相冲突,未来可能改变。
变量命名约定
常见 QEMU 类型有一些短变量名惯例。 架构无关 CPUState 指针通常叫 cs,具体 CPUArchState 指针通常叫 env。 设备模拟中的通用 DeviceState 通常叫 dev。
函数命名约定
标准库或 GLib 的封装函数使用 qemu_ 前缀,提醒读者并非原函数,
例如 qemu_strtol、qemu_mutex_lock。
其他跨代码库广泛使用的工具函数不应带前缀,
例如 pstrcpy、find_first_bit 等位操作。
修改全局模拟器状态的函数也用该前缀,例如 qemu_add_vm_change_state_handler。 若存在明显的子系统专用前缀,应优先使用。
文件或子系统公开、在头文件声明的函数,通常使用一致前缀表明归属,
例如 cputlb.c 的 tlb_,或 cpus.c 的 cpu_。
同一功能有持锁和不持锁两个版本时,预期调用者已持锁的版本通常加 _locked。
兼容性适配函数使用 _compat,通常不直接调用,而经预处理器别名映射到普通名称。 _impl 用于不直接调用、而经宏或内联函数使用的具体实现。
块结构
所有缩进语句都必须带花括号,即使只有一条语句。 左括号与控制流语句同一行;右括号与 else 同行,没有 else 时单独一行。例如:
if (a == 5) {
printf("a was 5.\n");
} else if (a == 6) {
printf("a was 6.\n");
} else {
printf("a was something else entirely.\n");
}
else if 视为单条语句,否则长条件链会不断增加缩进。
函数左括号是例外,出于传统和清晰度,单独一行:
void a_function(void)
{
do_something();
}
一致的括号风格减少歧义,也避免增删行引发不必要改动。 此外,这就是 QEMU 风格。
声明
通常不允许块内声明与语句交错,声明应位于块开头。 为避免意外复用,可在 for 中声明循环变量:
for (int i = 0; i < ARRAY_SIZE(thing); i++) {
/* do something loopy */
}
偶尔允许 #ifdef/#ifndef 内声明作为例外: 如果更清楚,即使外面已有语句,也可在条件块开头声明。 不过通常更适合把整个条件块移到独立函数。
条件语句
变量与常量比较相等或不等时,常量放右侧,例如:
if (a == 1) {
/* Reads like: "If a equals 1" */
do_something();
}
理由:if (1 == a) 这类尤达式条件难读。 好的编译器即使常量在右侧,也能对误把 == 写成 = 发出警告。
语言用法
预处理器
可变参数宏
使用类似 C99 的形式:
#define DPRINTF(fmt, ...) \
do { printf("IRQ: " fmt, ## __VA_ARGS__); } while (0)
包含指令
包含顺序如下:
#include "qemu/osdep.h" /* Always first... */
#include <...> /* then system headers... */
#include "..." /* and finally QEMU headers. */
qemu/osdep.h 定义影响 stdint.h 等核心系统头行为的宏,必须最先包含, 确保外部库包含系统头时也得到 QEMU 依赖的宏。
不要从头文件包含 osdep.h,因为 .c 已经包含。
头文件通常应自行包含除 osdep.h 外所需的一切。 如有例外,必须在头文件中说明。 若只需要某头文件的 typedef,可以考虑将其移到 qemu/typedefs.h。
禁止循环包含。
用于代码生成的包含文件
QEMU 广泛通过宏预处理器实例化相似函数。 不禁止这种做法,但它可能使调试和代码导航更难。 应考虑能否通过方便编译器常量折叠,或用 Python 生成便于 grep 的代码达到同样效果。
模板头文件应使用 .c.inc 或 .h.inc 后缀,明确表示为展开而包含。
C 类型
应使用正确类型,以下是一些实用建议。
标量
准备使用 int 或 long 时,很可能存在更适当的类型。 计数变量应使用无符号类型。
主机内存大小通常使用 size_t,只有必要时用 ssize_t。 客户机 RAM 偏移必须用 ram_addr_t,但它只覆盖 RAM,不一定覆盖全部客户机地址空间。
文件大小或有符号文件偏移使用 off_t。 只计数较小数值时用 unsigned int;除少见嵌入式系统外,可假设至少四字节。
要求特定位宽时使用 int32_t、uint32_t、uint64_t 等标准类型。 VMState 字段必须使用这些固定宽度类型。
不要使用 Linux 内部类型 u32、__u32、__le32。
客户机物理地址用 hwaddr,PCI 地址例外,使用 pcibus_t。 ram_addr_t 是 QEMU 内部的中间地址空间,将客户机 RAM 物理地址映射到主机虚拟地址。 客户机内存大小通常可放入它,但不能用它保存真实客户机物理地址。
CPU 虚拟地址有几种类型: vaddr 最适合目标无关代码,足以容纳任何目标的虚拟地址,大小不随目标变化,且无符号。 target_ulong 大小与目标 CPU 虚拟地址相同,可能为 32 或 64 位, 因此只用于目标专用代码,以及 TLB 等性能关键、按目标编译的核心代码。 对应有符号版本是 target_long。 abi_ulong 用于用户模式目标,大小与目标 ABI 的 void 指针相同。 例如 sparc32plus 在 64 位 CPU 上使用 32 位指针,可能不同于完整虚拟地址宽度。 必须匹配目标 ABI 的结构,对目标 unsigned long 或指针字段应使用该类型。 有符号版本为 abi_long。
这些建议需要结合情况。 调用要求 size_t、pid_t、off_t 等类型的系统接口时,相关变量应匹配接口。 若改用 unsigned int 与相关变量符号属性冲突, 而全面修正会牵涉过大,也可能宁可暂时使用“不理想”的类型。
描述性类型很重要,但不要过度。 如果造成警告或需要强制转换,应重新考虑或寻求帮助。
指针
所有指针应保证 const 正确性。 不用于修改目标存储时,加 const,让读者立即知道它只读。 坚持这一点后,非 const 指针就明确表示会修改目标, 或作为另一可修改指针的别名。
Typedef
typedef 用于消除冗余 struct 关键字, 因为类型用 CamelCase,与其他 snake_case 标识符已有区别。 每个命名结构应有 CamelCase 名称和对应 typedef。
部分编译器不接受重复 typedef,因此只在一个头文件声明。 公共类型可放 include/qemu/typedefs.h。 为方便起见,头文件和原型也可直接使用结构前向声明, 避免重复 typedef,并减少头文件之间的包含。
位域
C 位域可能导致不可移植,尤其 Windows 上 MSVC 与 GCC 的布局不同, 或需要考虑字节序时。
因此,紧凑结构和必须精确匹配客户机内存布局的结构禁止使用位域。 已有代码可能使用,但其布局已经仔细确认。
即使布局无关,也建议避免位域,除非能证明有显著易用性收益。 建议改用安全的 include/hw/core/registerfields.h。
C 与 POSIX 保留命名空间
避免下划线加大写、双下划线,以及 _t 后缀。
底层内存管理
禁止使用 malloc/free/realloc/calloc/valloc/memalign/posix_memalign, 以及 alloca/g_alloca/g_newa/g_newa0。 应使用 GLib 的 g_malloc/g_malloc0/g_new/g_new0/g_realloc/g_free, 或 QEMU 的 qemu_memalign/qemu_blockalign/qemu_vfree。
g_malloc 分配失败会退出,因此无需像 malloc 那样检查。 启动时通常可以使用,因为失败本来就致命; 但推测性加载大型调试符号表等启动场景,也可能不应因此退出。
避免让客户机通过触发大额分配导致 QEMU 退出。 约 4K 的小额分配失败通常意味着主机过载,允许退出合理。 较大分配若可回退到更小方案,应使用 g_try_new 并检查结果, 例如 SoftMMU TLB 的 tlb_mmu_resize_locked 所做的时间/空间权衡。
分配生命周期只限当前函数,且有多个退出路径时,可用 g_autofree 等改善可读性, 见 自动释放内存。
g_malloc(0) 合法并返回 NULL。
优先 g_new(T, n),而非 g_malloc(sizeof(T) * n),因为:
检测乘法是否溢出 size_t;
返回 T 指针而非 void 指针,编译器能发现更多类型错误。
不过以下声明也可接受:
T *v = g_malloc(sizeof(*v))
qemu_memalign 或 qemu_blockalign 分配的内存必须用 qemu_vfree 释放, 否则会在 Win32 出现问题。
字符串处理
不要使用 strncpy。它不保证 NUL 结尾,十分危险, 还会将目标剩余字节一直清零到指定长度。 尽可能使用以下类似函数,但注意签名不同:
void pstrcpy(char *dest, int dest_buf_size, const char *src)
不要使用无法检查缓冲区溢出的 strcat,改用:
char *pstrcat(char *buf, int buf_size, const char *s)
sprintf、vsprintf 有同样问题,应改用 snprintf、vsnprintf。
QEMU 还提供其他实用字符串函数:
int strstart(const char *str, const char *val, const char **ptr)
int stristart(const char *str, const char *val, const char **ptr)
字符处理也有替代 isxyz/toxyz 的宏,例如使用 qemu_isalnum 而非 isalnum。 根据内存管理规则,必须使用 g_strdup/g_strndup,而非普通 strdup/strndup。
Printf 风格函数
新增带格式字符串及后续可变参数的函数时, 必须在原型上使用 GCC printf 属性, 使 -Wformat 和 -Wformat-security 能检查格式与参数数量、类型是否一致。
C 标准及实现定义、未定义行为
QEMU C 代码应符合 C11,最终标准的草案形式可从以下位置下载:
C 规范保留未定义和实现定义行为,为编译器优化提供空间。 通常代码应遵守规范,避免两者;“在我测试的 GCC 上没问题”不是有效理由。 不过部分行为在所有关注的平台上都一致,而严格可移植写法很麻烦,因此允许假定:
整数采用二进制补码;
有符号整数右移复制符号位,即算术右移。
另外,QEMU 假定编译器不利用 C99/C11 将部分有符号左移视为未定义行为的自由, 相关行为自 GCC 4.0 起记载于其手册。
自动释放内存
QEMU 必须使用 GCC 或 Clang,因此可以使用 C 扩展, 在栈变量离开作用域时自动调用清理函数。 自动释放内存能简化清理路径,常可减少大量 goto。
GLib2 提供相应函数和宏:
主要包括:
g_autofree:离开作用域时调用 g_free()。
g_autoptr:对结构或对象调用通过 G_DEFINE_AUTOPTR_CLEANUP_FUNC 定义的清理函数,大多数 GLib 类型和 GObject 均支持。
例如,以下代码:
int somefunc(void)
{
int ret = -1;
char *foo = g_strdup_printf("foo%", "wibble");
GList *bar = .....
if (eek) {
goto cleanup;
}
ret = 0;
cleanup:
g_free(foo);
g_list_free(bar);
return ret;
}
可以改写为:
int somefunc(void)
{
g_autofree char *foo = g_strdup_printf("foo%", "wibble");
g_autoptr (GList) bar = .....
if (eek) {
return -1;
}
return 0;
}
虽然通常更简单、更不易泄漏,仍需注意:
g_auto*声明的变量必须初始化,否则清理函数会使用未初始化栈内存。若其中的值需要超出函数生命周期,必须先保存该值,再把原变量置 NULL。 g_steal_pointer 可以简化操作。
char *somefunc(void)
{
g_autofree char *foo = g_strdup_printf("foo%", "wibble");
g_autoptr (GList) bar = .....
if (eek) {
return NULL;
}
return g_steal_pointer(&foo);
}
QEMU 专用惯用法
QEMU 对象模型声明
QOM 为基础 C 提供对象框架。 实例存储结构或类结构的第一个成员必须是父对象,并与新成员之间留空行。 属性后备字段,即用户选项,与内部状态也宜分开,便于阅读。
实例结构首成员应叫 parent_obj,类结构首成员应叫 parent_class,例如:
struct MyDeviceState {
DeviceState parent_obj;
/* Properties */
int prop_a;
char *prop_b;
/* Other stuff */
int internal_state;
};
struct MyDeviceClass {
DeviceClass parent_class;
void (*new_fn1)(void);
bool (*new_fn2)(CPUState *);
};
QOM 结构无需手动 typedef,声明宏会自动生成,详情见 QEMU 对象模型(QOM)。
QEMU GUARD 宏
_GUARD 宏简化多退出路径处理。
例如用 QEMU_LOCK_GUARD 加锁,会确保函数退出时释放:
static int my_critical_function(SomeState *s, void *data)
{
QEMU_LOCK_GUARD(&s->lock);
do_thing1(data);
if (check_state2(data)) {
return -1;
}
do_thing3(data);
return 0;
}
无论怎样退出,s->lock 都会释放。 不使用宏的等效代码必须在每个出口正确调用 qemu_mutex_unlock():
static int my_critical_function(SomeState *s, void *data)
{
qemu_mutex_lock(&s->lock);
do_thing1(data);
if (check_state2(data)) {
qemu_mutex_unlock(&s->lock);
return -1;
}
do_thing3(data);
qemu_mutex_unlock(&s->lock);
return 0;
}
通常还有 WITH_ 形式,便于包围函数内部代码块:
WITH_RCU_READ_LOCK_GUARD() {
QTAILQ_FOREACH_RCU(kid, &bus->children, sibling) {
err = do_the_thing(kid->child);
if (err < 0) {
return err;
}
}
}
错误处理和报告
向用户报告错误
不要使用 printf()、fprintf() 或 monitor_printf(), 应使用 error-report.h 的 error_report() 或 error_vreport(), 确保在正确位置,即当前监控器或标准错误,以统一格式报告。
额外信息用 error_printf() 等输出。
error_report() 输出当前位置。命令行解析等常见场景会自动跟踪位置,
手动调整则使用 error-report.h 的 loc_*()。
传播错误
错误常不能在发现处直接报告,而需沿调用链传到合适的处理位置,方式有多种。 最灵活的是 Error 对象,用法见 error.h。
采用最简单合适的成功/失败传递方式,遵循常见约定: 非负/-1、非负/-errno、非空/NULL,或 Error 对象。
例如函数成功返回指针,且对调用者只有一种失败方式时,
失败返回 NULL 就足够,比传递 Error 对象简单清楚。
若调用者需要只有函数内部才知道的详细原因,则用 Error ** 设置适当错误。
不要一边返回错误供其他代码处理,一边向用户报告。 应由最终消费错误的位置负责报告。
处理错误
启动配置错误时可以调用 exit(),正常运行时则有问题,监控器命令绝不能 exit()。
客户机可触发的错误,例如未实现的翻译或设备模拟边界情况, 不得通过 exit() 或 abort() 处理,客户机不应有能力终止 QEMU。
&error_fatal 等价于 exit(1),&error_abort 等价于 abort()。
trace-events 风格
0x 前缀
跟踪事件中,十六进制应带 0x,例如:
some_trace(unsigned x, uint64_t y) "x 0x%x y 0x" PRIx64
按惯例以十六进制表示、用点、斜杠、冒号或空格分隔的数字组可例外, 例如 PCI 总线 ID:
another_trace(int cssid, int ssid, int dev_num) "bus id: %x.%x.%04x"
这类数字组也可带 0x。无论如何,应让十六进制含义清楚,例如:
data_dump(uint8_t c1, uint8_t c2, uint8_t c3) "bytes (in hex): %02x %02x %02x"
没有前缀时,十六进制日志难读,尤其数字中恰好没有字母,或同一行还包含十进制。 允许数字组省略,是因为 %x.%x.%x 等惯例不只 QEMU 使用。 原始字节转储逐字节加 0x 也更难读。
printf 的 # 标志
不要使用 %#x 等 # 格式标志。
添加前缀有 0x%... 和 %#... 两种方式,为一致应统一使用前者,因为:
更常见;
%# 对零值省略 0x,使输出不一致。
注释风格
使用传统 C 的
/* */,避免 //。 后者在 C99 合法,这只是风格一致性要求,checkpatch 会提醒。多行注释左侧应有星号,起止标记各占一行:
与 Linux 内核风格相同。
已有代码有时采用左侧无星号的 GNU 风格或其他形式。 新注释不要仿照;除非本来就要编辑该注释,也不必专门转换。
理由是保持一致,并便于从周围代码中辨识多行注释。