int load(const char *path)
{
FILE *f = fopen(path, "r");
if (!f)
return -1;
defer fclose(f);
char *buf = malloc(4096);
if (!buf)
return -1;
defer free(buf);
/* 业务逻辑 */
return 0;
}
用 C6 给 C 语言装上 defer、try/catch、async/await、泛型和现代内存管理。
如果你写过 C 语言,一定遇到过这些问题:
-
malloc 之后,时刻担心忘记 free。
-
写一个错误处理函数,满屏都是 if 和 goto。
-
想用 async/await?C 语言原生不支持。
-
想用泛型?只能自己折腾宏。
-
想写模块化代码?头文件、源文件、符号管理让人头疼。
-
想管理复杂对象的生命周期?只能自己处理指针和引用。
有没有想过:
如果 C 语言也能使用 defer、try/catch、async/await、泛型、接口、闭包和引用计数式内存管理,会是什么体验?
现在,你可以试试 C6。
C6 是一个基于 TinyCC 扩展的编译器项目。它希望在保留普通 C 文件原有语义的基础上,为 C 增加现代语言特性。
最重要的是:不需要抛弃你熟悉的 C 语言。
接下来,用 15 分钟,通过几个真实示例,看看 C 能变成什么样。
一、先跑起来:3 条命令体验 C6
项目地址:
https://github.com/c6lang/c6
克隆项目:
git clone https://github.com/c6lang/c6.git
cd c6
编译:
make build
项目需要基本的 C 编译工具链和 Make。不同平台的依赖可能有所区别。
编译完成后,可以直接运行项目自带的示例:
./tcc/tcc -B tcc -run examples/err_demo.c6
再试试异步示例:
./tcc/tcc -B tcc -run examples/async_demo.c6
以及内存管理示例:
./tcc/tcc -B tcc -run examples/gc_demo.c6
注意:异步 I/O 示例涉及 POSIX 环境,Windows 等非 POSIX 平台可能需要额外适配。
这里有一个重要概念:
C6 使用 .c6 作为扩展语言的开关。
普通 .c 文件仍然保持普通 C 的语义,扩展语法只在 .c6 文件中启用。
二、2分钟体验 defer:终于不用到处写 cleanup 了
C 语言中,资源释放经常是一个令人头疼的问题。
例如打开文件后,需要在多个返回路径中确保文件被关闭:
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
return -1;
}
/* 处理文件 */
fclose(fp);
如果函数中间有多个错误返回路径,就很容易漏掉资源释放。
传统 C 代码经常需要使用 goto cleanup。
C6 提供了 defer:
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
return -1;
}
defer fclose(fp);
/* 处理文件 */
当离开当前作用域时,defer 会自动执行。
它的执行顺序是后进先出(LIFO),类似栈式清理。
例如:
defer cleanup_a();
defer cleanup_b();
defer cleanup_c();
执行顺序是:
cleanup_c()
cleanup_b()
cleanup_a()
不仅是正常 return,项目还支持在 break、continue、goto 和错误返回等控制流退出时执行相应清理。
defer 是怎么实现的?
这部分其实很有意思。
C6 并不是简单地在运行时维护一个动态链表。
它会在编译阶段确定清理点,并通过函数栈帧中的状态信息记录哪些清理操作需要执行。
项目文档描述了函数帧局部位图和清理派发机制,并对每个函数的清理点数量及嵌套深度设置了限制。
这意味着:
defer 的清理逻辑可以在编译阶段安排,不需要为每次 defer 注册动态分配内存。
对于习惯了 C 语言手动资源管理的开发者来说,这是一个很实用的改进。
三、2分钟体验错误处理:try / catch / err
C 语言的错误处理方式比较自由,但也容易出现大量重复代码。
C6 增加了错误处理语法:
-
err:返回错误。
-
try:尝试执行,并传播错误。
-
catch:捕获并处理错误。
-
T!error:表示带错误返回能力的函数类型。
例如:
return err(id, msg);
可以用来返回错误。
使用 try 可以传播错误,而 catch 可以捕获错误并执行对应处理。
具体示例可以直接查看项目中的:
examples/err_demo.c6
这里值得注意的是,C6 的错误处理并不是简单地把 C 的返回值换一种写法。
它有自己的错误状态和传播机制。
try 负责传播错误,catch 可以获取错误信息并处理。对于需要返回错误的函数,错误返回类型也参与编译器的检查。
这样就能减少大量重复的错误判断代码。
四、2分钟体验 async / await:让 C 也能写异步代码
这是 C6 最吸引人的特性之一。
在很多现代语言中,异步编程已经成为常见能力:
async
await
但传统 C 语言并没有原生提供这样的语法。
C6 引入了:
可以通过项目自带的异步示例快速体验:
./tcc/tcc -B tcc -run examples/async_demo.c6
它是多线程吗?
这里一定要区分两个概念。
async / await 是协程,不等于创建新线程。
C6 的异步任务采用无栈协程设计。任务对象中保存了协程需要的原生帧和恢复状态,等待条件满足后可以继续执行。
它属于协作式调度,不能简单理解为多个 CPU 核心同时执行。
那么,真正的线程怎么创建?
C6 还有一个 spawn:
spawn
它使用 pthread_create 创建操作系统线程。
因此:
|
特性
|
作用
|
|
async / await
|
协程式异步执行
|
|
spawn
|
创建操作系统线程
|
|
join
|
等待线程结束
|
这两个机制解决的是不同问题。
另外,C6 的 c6_io 使用 select() 等待文件描述符就绪。它不是把任意阻塞系统调用自动变成异步调用,实际使用时仍需要考虑非阻塞 I/O。
五、2分钟体验泛型:C 语言也能写泛型容器
传统 C 语言没有原生泛型。
如果要写一个可以保存不同类型数据的容器,通常需要:
-
使用 void *
-
使用宏
-
为不同类型重复实现
-
自己处理类型转换
C6 提供了泛型支持。
它通过 @ 语法应用泛型类型,并在编译期进行单态化。
例如,泛型类型可以针对具体类型生成对应的普通 C 类型和符号。
项目标准库中的 Vect 就是一个例子。
import std.vec;
@Vect@int v = new Vect@int;
v->push(10) catch e {
return e.id;
};
printf("%d\n", v->at(0));
这里使用的是整数类型的 Vect。
编译器会根据具体类型生成对应实现,而不是在运行时进行通用类型分发。
这类技术通常称为 单态化(Monomorphization)。
它的好处是:泛型代码最终可以转化为普通的具体类型和函数调用。
六、2分钟体验现代内存管理:@T、shared、weak、own
这是 C6 另一个值得关注的设计。
传统 C 语言需要开发者手动管理对象生命周期。
C6 提供了一套引用计数式托管内存机制。
主要包括:
|
类型
|
含义
|
|
@T
|
普通引用计数
|
|
shared @T
|
原子引用计数,可用于跨线程共享
|
|
weak @T
|
弱引用,不持有对象生命周期
|
|
own @T
|
独占所有权,不进行引用计数
|
同时还提供:
new
move()
drop
分别用于创建对象、转移所有权和释放对象。
它是不是传统 GC?
严格来说,不是常见的追踪式垃圾回收器。
它主要使用引用计数管理对象生命周期,并结合编译期检查处理引用关系。
项目还提供了引用环检测机制。
例如,强引用环可能导致对象无法正常释放,因此需要使用 weak 或调整所有权结构。
这套机制并不是传统意义上的 Mark-Sweep,也不是 Stop-the-World 垃圾回收。
更准确的描述是:
引用计数式托管内存管理。
对于希望减少手动 malloc/free,又不想完全放弃 C 语言控制力的开发者来说,这是一条值得探索的路线。
可以运行:
./tcc/tcc -B tcc -run examples/gc_demo.c6
查看完整示例。
七、还有哪些现代语言特性?
除了前面介绍的功能,C6 还提供了不少扩展。
1. interface
接口采用胖指针和虚函数表(vtable)实现。
概念上可以理解为:
对象指针 + 方法表
它让 C 语言也能拥有一种接口抽象机制。
2. slice
C6 支持切片:
T[:]
切片包含元素指针和长度,并提供运行时越界检查。
这比单纯传递一个裸指针更容易表达数组范围。
3. foreach
提供更方便的集合遍历语法。
4. module / import
支持模块和导入机制。
项目采用文件路径作为模块名,一个文件对应一个模块。
需要注意,import 引入声明,并不意味着模块实现文件可以不参与编译。
5. closure
支持函数字面量和捕获式闭包。
捕获式闭包可以保存外部变量的值,成为托管对象。
不过它有生命周期和线程使用限制,不能简单等同于可以任意跨线程传递的函数对象。
6. 宏
C6 还提供声明式宏。
宏参数可以指定为不同角色:
expr
stmt
ident
type
并支持宏局部变量的卫生重命名。
这让宏不只是简单的文本替换。
八、最有意思的设计:C6 可以生成 C99 代码
如果只看语言特性,C6 可能只是一个给 C 语言增加功能的编译器。
但它还有一个值得关注的设计:
C6 不仅可以生成机器码,还可以生成 C99 源代码。
项目提供了第二套代码生成后端:
tcc/c99-gen.c
它复用了前端的预处理器、解析器和 C6 扩展语法,最终输出 C99 代码。
例如:
./tcc/tcc -B tcc -o out.c program.c6
然后再使用 GCC 或 Clang 编译生成的 C 文件。
这种设计有几个好处:
-
可以把 C6 当作一门扩展 C 的源语言。
-
可以借助 GCC、Clang 等成熟工具链完成后续编译。
-
生成的代码更容易检查和调试。
-
不必把所有目标平台的机器码生成工作都放在 C6 自己身上。
当然,这个后端也有平台和 ABI 边界,并不是所有平台、所有 C 编译器和所有混合链接场景都能无条件兼容。
例如,异步 I/O 和线程运行时涉及 POSIX 能力,部分结构体 ABI 场景也有明确限制。
九、它的工程成熟度如何?
一个语言项目是否值得关注,不能只看 README 中列出了多少功能。
还要看测试、代码生成和自举能力。
C6 的 Makefile 提供了较完整的测试入口:
make check-all
根据项目最新提交记录,维护者报告这一轮检查覆盖了:
-
selftest
-
tests2
-
C6 自检
-
标准库测试
-
C99 后端测试
-
TCC 编译 C99 输出的测试
-
多条自举路线
最新提交还报告三条自举路线生成的目标文件逐字节一致。
这里需要说明:这是仓库提交记录中的测试结果,不代表我在当前环境中独立执行了全部测试。
但从工程设计上看,C6 不只是一个语法演示项目,它已经在尝试验证前端、多个代码生成后端和自举流程之间的一致性。
十、15分钟之后,值得关注什么?
C6 最有意思的地方,并不是它增加了多少关键字。
而是它试图回答一个问题:
C 语言能不能在不放弃底层控制力的前提下,获得现代语言的开发体验?
它的思路不是重新发明一门完全不同的语言,而是在 TinyCC 的基础上扩展:
-
保留 C 的基础编程方式。
-
用 defer 改善资源清理。
-
用 try/catch 改善错误处理。
-
用 async/await 引入协程。
-
用 spawn 支持操作系统线程。
-
用泛型和 interface 改善代码复用。
-
用引用计数式托管内存改善对象生命周期管理。
-
用 C99 后端拓展代码生成和工具链选择。
当然,C6 目前仍然有自己的限制。
它不是一个可以直接替代所有 C 编译器的成熟通用工具链,也不意味着现有 C 项目可以不做任何修改就迁移。
如果你正在做嵌入式、系统软件、工具链开发,或者只是对 C 语言的未来方向感兴趣,C6 值得亲自跑一遍。
最后:动手试试
项目地址:
https://github.com/c6lang/c6
建议从这三个示例开始:
./tcc/tcc -B tcc -run examples/err_demo.c6
./tcc/tcc -B tcc -run examples/async_demo.c6
./tcc/tcc -B tcc -run examples/gc_demo.c6
15 分钟,未必能让你彻底理解一个编译器。
但足够让你看到:
原来 C 语言,也可以拥有这些现代语言特性。
如果你喜欢 C,又希望它能拥有更现代的编程体验,不妨给 C6 一个机会。