Fil-C 0.680 新增了 getcontext、setcontext、makecontext、swapcontext 等 ucontext APIs。想试这组接口,现在还不能只等现成包,开发者需要从源码构建 Fil-C。
这条更新有个反常点:越是老 C 程序熟悉的上下文切换接口,越难安全支持。setjmp/longjmp 和 ucontext 都能让程序绕过普通函数调用路径。它们一旦恢复到已经返回的函数栈帧,或者跳到已经释放的线程栈上,程序就可能在一段“死栈”上继续执行。
我更在意的不是 Fil-C 又补了几个 API,而是它把这类错误从“未定义行为里碰碰运气”,改成了“运行时能检查,越界就 panic”。这对写 C/C++ 基础库、协程框架和运行时的人更关键。
危险不在 API 名字,而在能不能跳回死栈
setjmp/longjmp 常被 C 程序拿来做异常处理。ucontext APIs 则更常出现在协程、fiber 这类实现里,Boost Fiber 也曾把 ucontext 作为可选后端之一。
它们的共同点很直接:保存寄存器、栈指针和指令位置,然后在之后恢复执行。普通函数调用是一层层进、一层层出;这些接口可以中途改道。
问题也从这里来。传统 C 里,如果保存上下文的函数已经返回,恢复它就是跳回旧栈帧。如果线程栈已经释放,再恢复到那条栈上,就是悬空栈执行。轻则读到垃圾数据,重则破坏栈和控制流。
Fil-C 的处理路线不是假装这些用法都安全。它更像给老接口加了硬边界:合法路径继续跑,危险恢复直接拒绝。
| 场景 | 传统 C 风险 | Fil-C 的处理 | 对迁移的影响 |
|---|---|---|---|
longjmp 跳回旧位置 | 可能恢复已返回函数的栈帧 | 检查目标 zjmp_buf 是否属于当前调用栈祖先帧,不满足就 panic | 依赖非常规跳转的代码要改 |
间接调用 setjmp | 编译器可能识别不到 returns_twice | 要求 setjmp 只能被直接调用 | 函数指针封装要清理 |
直接操作 jmp_buf | 可能伪造或篡改跳转状态 | jmp_buf 只是指向不透明 zjmp_buf 的入口 | 依赖内部布局的代码不保证兼容 |
| ucontext 手工栈 | 栈释放后仍可能被恢复 | Fil-C 在能力模型内管理合法上下文,误用会被阻断 | 协程库要重新检查栈生命周期 |
这张表背后的判断很简单:Fil-C 没有追求纵容所有旧写法。它更愿意让不安全代码在边界处停下,而不是继续执行到栈已经坏掉。
setjmp 的坑,编译器必须提前知道
很多人说 setjmp 危险,是因为它会“返回两次”。这句话对,但还没说到根上。
真正麻烦的是,编译器必须提前知道某个调用会 returns_twice。只有这样,它才能保守处理寄存器保存、局部变量生命周期和 spill slot 复用。
如果把 setjmp 藏进函数指针里再调用,编译器可能无法可靠识别它。后果是,一个变量在 setjmp 前用过的 spill slot,可能被后面的变量复用。等 longjmp 回来,程序以为自己回到了旧现场,实际现场已经被改写。
Fil-C 的约束很硬:setjmp 只能被直接调用。这样编译器才能触发专门逻辑,避免 spill slot 生命周期被错误处理。
它还把 jmp_buf 做成指向不透明 zjmp_buf 的入口。内部状态只由 Fil-C 运行时维护,用户不能把 jmp_buf 当普通内存改,也不能伪造一个跳转目标再交给 longjmp。
这里要区分一件事:panic 不是“又多了一个崩溃漏洞”。在这个语境下,它是安全边界生效。Fil-C 选择在危险恢复发生时叫停,而不是让程序带着坏栈继续跑。
对维护者来说,动作也很具体。迁移前要搜三类代码:
- 是否通过函数指针、宏封装或间接层调用
setjmp; - 是否读取、复制、覆盖或假设
jmp_buf的内部布局; - 是否把
longjmp当成跨线程、跨栈、跨生命周期的通用跳转工具。
如果这些用法存在,团队不该急着把项目切到 Fil-C 验证性能。更稳的做法是先清理上下文切换路径,再评估兼容性成本。
ucontext 能跑,不等于该把旧债搬过来
ucontext APIs 的位置比 setjmp/longjmp 更尴尬。它适合做协程和 fiber,但并不是所有系统都推荐继续使用。Darwin 等系统已经弃用,glibc 仍支持,所以 Linux 生态里的遗留项目和底层库还绕不开它。
Fil-C 0.680 支持 ucontext,更像现实妥协。它让依赖 getcontext、setcontext、makecontext、swapcontext 的代码,有机会进入 Fil-C 的内存安全模型,而不是一遇到上下文切换就退回不受控状态。
但这个支持有边界。手工管理栈的代码,尤其要检查 makecontext 指向的栈是否仍然有效。栈生命周期一旦和上下文恢复脱节,Fil-C 不会为了兼容而放行危险路径。
最受影响的是两类人。
一类是协程库、fiber 框架、语言运行时的维护者。他们要决定是继续保留 ucontext 后端,还是把默认后端切到更可控的实现。短期动作会是增加 Fil-C 构建测试,隔离手工栈管理代码,而不是直接宣布全量兼容。
另一类是评估内存安全 C 方案的团队。他们需要把“能否编译通过”拆成更细的问题:是否依赖 jmp_buf 内部布局,是否间接调用 setjmp,是否把上下文对象跨线程或跨栈生命周期保存。
接下来最该看的不是 Fil-C 还能补多少老 API,而是三个更硬的变量:
- ucontext 支持什么时候进入更易安装的发行路径,而不是主要靠源码构建;
- 编译器对错误
setjmp用法能否给出清晰诊断,而不是让开发者只看到内部错误; - 真实项目迁移时,暴露最多的是
jmp_buf布局依赖,还是手工栈生命周期问题。
这三个变量决定了 Fil-C 的 ucontext 支持是少数底层项目能用的安全垫,还是能成为更多旧 C/C++ 代码迁移时的实际路径。
