Fil-C 0.680 新增了 getcontextsetcontextmakecontextswapcontext 等 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,更像现实妥协。它让依赖 getcontextsetcontextmakecontextswapcontext 的代码,有机会进入 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++ 代码迁移时的实际路径。