Zig Creator Andrew Kelley Talks Zig’s IO Design and Function Coloring Problem (2025-10-10, gemini-2.5-pro)
1. 导读
在这期对话中,两位新一代系统编程语言的创造者——Zig的Andrew Kelley与Roc的Richard Feldman——进行了一场关于语言设计底层逻辑的深度交流。这场讨论之所以重要,不仅因为Roc正在用Zig重写其整个编译器,将Zig的哲学推向极限,更因为它发生在行业对“内存安全”的讨论近乎固化为“是否拥有借用检查器”的当下。对话的核心,是关于一系列看似激进、回归第一性原理的设计选择:从引发社区争议的IO重构,到挑战操作系统边界的编译器缓存与热重载机制。
这场对话揭示了一种不同于主流安全叙事的系统编程世界观,它押注于程序员的纪律性,并通过提供极致的控制力和精巧的工具来赋能他们。它将影响那些正在为性能敏感型项目选择技术栈的架构师、探索语言设计新边界的研究者,以及所有对“什么是真正的代码质量与开发效率”抱有疑问的资深工程师。当所有人都认为安全与性能是一场零和博弈时,这场对话却在追问:是否存在一条被遗忘的、能同时优化两者的第三条路?
2. 核心观点
Andrew Kelley的核心世界观是:编程语言的终极职责是帮助开发者编写出最符合机器真实运行方式的、性能无损的代码,并为其提供管理这种复杂性的工具,而非用抽象层将其隐藏。这套哲学将控制权和责任感完全交还给程序员,认为真正的安全源于代码的简洁、可预测和对底层机制的深刻理解,而非仅仅依赖于编译器的静态保证。这种“C语言精神的现代复兴”是有争议的,因为它挑战了过去十年由Rust引领的“静态内存安全至上”的行业共识,对开发者的技能和纪律性提出了极高的要求,可能被视为一种向“人治”的倒退。
判断一:显式IO参数传递无关“函数颜色”,关乎架构的可测试性与灵活性
- 断言:Zig最新的IO设计要求任何执行IO操作的函数都必须显式接收一个
IO参数。这并非async/await所引发的“函数颜色”问题(即同步与异步代码的传染性分裂),而是一项根本性的架构决策。 - 逻辑:Kelley认为,核心问题不在于多写一个参数,而在于“你的标准库是否需要一个平行的异步版本”。通过将IO能力作为参数注入,代码与具体执行环境(同步阻塞、异步事件循环、或模拟的测试环境)解耦。这使得切换IO模型成为一个非破坏性的变更,并且极大地简化了测试——开发者可以轻易地传入一个模拟的
IO实现来测试各种边缘和故障场景,例如注入文件未找到或网络超时等错误。 - 佐证:对话中提到,这种模式与Zed编辑器在Rust代码中传递
FS(文件系统)对象用于测试的实践如出一辙。Zig内部也计划利用此机制,实现类似“测试所有内存分配失败场景”的IO故障注入测试。
判断二:极致性能源于对内存布局的绝对控制,而非仅仅算法优化
- 断言:实现编译器等复杂系统的高性能,关键在于采用“索引替代指针”的数据结构设计,从而获得对内存布局的完全控制,实现高效的缓存、序列化与跨进程共享。
- 逻辑:当数据结构(如AST)内部通过索引而非指针相互引用时,整个结构在内存中可以被组织成少数几个连续的大块。这使得序列化到磁盘可以简化为一次
pwritev系统调用,将多个内存块直接“拍”到文件中,几乎零成本。反序列化时,只需将整个文件读入内存,然后通过简单的地址偏移修正少量顶层“指针”(实际上是内存块的基地址),即可瞬间“复活”整个数据结构。 - 佐证:Roc编译器的重写正是基于这一理念,目标是每个模块仅有约20个顶级指针,其余均为索引。这一设计不仅为了极速的磁盘缓存,更服务于其雄心勃勃的“共享内存热重载”架构,即编译器进程直接将内存块共享给运行中的子进程,后者只需进行指针修复即可执行新代码。
判断三:并发(Concurrency)与异步(Asynchrony)是两种不同且必须在代码中明确区分的需求
- 断言:引用Loris Cro的观点,Kelley强调了并发与异步的细微但关键的区别。异步是指操作被允许乱序执行,但也可以顺序执行;而并发则是指操作必须非阻塞地交错执行,否则会导致死锁。
- 逻辑:写两个独立文件是异步的——它们可以顺序阻塞执行,也可以并行执行。但在一个单线程测试中同时模拟服务器(accept)和客户端(connect),则需要并发——如果
accept阻塞地等待,connect代码将永远无法执行,导致死锁。因此,语言或库需要提供不同的原语(如Zig计划中的async concurrent await)来表达这种更强的“必须并发”的需求。 - 佐证:这个例子直接来源于系统编程的真实痛点,尤其是在编写需要自我交互的单元测试时。无法区分这两者会导致代码在某些执行引擎下正确,而在另一些引擎下则会死锁。
判断四:编译器警告是待修复的Bug,应当默认阻断流程
- 断言:将“未使用的变量”这类传统意义上的警告(Warning)升级为编译错误(Error)是保障代码质量的必要手段,因为它能强制开发者正视潜在的逻辑缺陷。
- 逻辑:在C/C++的开发文化中,警告经常被忽略,甚至在构建系统中被缓存而不再显示,导致潜在的bug被遗漏。Kelley认为,一个未使用的变量往往是“死代码”或逻辑错误的信号。Zig通过将其视为错误,迫使开发者在编码时就保持代码的整洁与正确。
- 佐证:Kelley分享了一个实例:他将一个有隐藏bug的C代码移植到Zig,仅仅因为Zig要求所有变量必须被使用(或显式忽略),就迫使他将变量声明为常量,从而立即通过编译器的“死代码存储”分析发现了原C代码中的bug。这个案例生动地展示了这一设计哲学如何将潜在的运行时错误转化为编译时错误。
这四个观点共同勾勒出Zig的设计哲学:通过强制开发者显式处理副作用(IO)、内存布局(索引)、执行模型(并发/异步)和代码质量(错误而非警告),来换取最终系统的可预测性、可测试性和极致性能。这是一条要求更高技艺的陡峭路径,但其承诺的回报是清晰和控制力。
3. 批判与质疑
Kelley的论述体系虽然逻辑自洽且充满洞见,但其有效性建立在一些关键的、未经大规模验证的前提之上,并有意无意地回避了某些固有风险。
首先,整个Zig哲学高度依赖于一个“理想程序员”模型。无论是手动管理IO依赖、精心设计内存布局,还是审慎处理每一个潜在的错误路径,都要求开发者具备高度的系统思维能力和 дисциплина (discipline)。这种模式对于经验丰富的系统程序员可能是赋能,但对于普通开发者或大型、技能水平不一的团队而言,可能会成为巨大的认知负担和错误来源。它所构建的“成功之坑”(pit of success)旁边,可能是一个更深、更易坠入的“复杂性深渊”。
其次,对话中对Roc编译器基于共享内存的“零拷贝”热重载架构的探讨,虽然技术上令人着迷,但也暴露了这种方法的极端脆弱性。该方案本质上是在手动模拟操作系统的动态链接器,但跨越了进程边界。任何一个指针修复的微小失误,都将导致难以调试的段错误或内存损坏。对话中Kelley对此表示赞赏但未深入诘问其鲁棒性,似乎默认了“只要程序员足够小心,就能正确实现”的乐观假设。这种“走钢丝”式的性能优化,其健壮性在复杂的真实世界场景下存疑。
再者,关于“反病毒软件是技能问题”的论断,虽然在个人开发者或小型初创公司情境下有其道理,但对于需要在严格安全策略(如企业环境、金融机构)下进行开发的程序员来说,这是一种过于简化的表述。能够任意修改运行中进程内存的技术,无论初衷如何,都极易被安全软件标记为恶意行为。将其归结为“关掉它”或“用户技能问题”,回避了在受控环境中推广此类高级开发工具的现实阻碍。
最后,对话结束时悬而未决的核心问题是:Zig所倡导的“通过显式控制和强大测试来保障安全”的模式,能否真正成为足以挑战“通过静态分析和借用检查来保障安全”模式的另一极?目前,这一论点更多地建立在哲学思辨和个案(如找到OpenZFS的bug)之上。当Zig和Roc生态系统发展壮大,吸引更多背景各异的开发者时,这种高度依赖个人能力的质量保证体系是否还能有效运转,仍是一个开放的问题。
4. 行业视野
这场对话为我们提供了一个精确的坐标,以理解Zig在当代编程语言演进图谱中的位置。
它首先印证了“机械共鸣”(Mechanical Sympathy)思潮的回归。在经历了多年抽象层次不断叠加的软件开发实践后,一股强调理解并顺应硬件行为(CPU缓存、内存对齐、系统调用成本)以获取极致性能的趋势正在回归。Zig对内存布局的执着,Roc对零拷贝IPC的探索,都是这一趋势的极致体现。这标志着对过去二十年“硬件足够快,程序员时间更宝贵”这一信条的反思,尤其是在云计算和大规模数据处理时代,每一分性能都可能被放大百万倍。
同时,这场对话激烈地挑战了由Rust确立的“内存安全”的单一叙事。自Rust出现以来,“安全”几乎等同于“由编译器静态保证的内存安全和线程安全”。Zig则代表了一种复古而又创新的挑战:它认为安全是一个更宽泛的概念,包括逻辑正确性、行为可预测性和资源管理的精确性。它主张,通过提供更简单、更直接的语言模型和强大的运行时检查工具(如测试分配器),可以在不引入借用检查器这种高复杂度抽象的情况下,帮助优秀的程序员写出同样安全甚至更可控的软件。这并非否定Rust的成就,而是开辟了另一条通往系统编程圣杯的道路,让人联想起C++社区中关于“现代C++可以通过RAII和智能指针实现安全”的长期论述,但Zig将其贯彻得更为彻底和纯粹。
最后,Roc编译器所设想的架构与一段值得铭记的历史形成了有趣的呼应。其通过共享内存和运行时代码注入实现的即时反馈与热重载,让人不禁想起Lisp Machines或Erlang/BEAM虚拟机所提供的动态、交互式开发环境。然而,Roc和Zig试图在没有任何虚拟机、直接面向裸金属的编译型语言中实现这一点。这可以被看作是试图将动态语言的灵活性与系统语言的性能进行一次前所未有的融合,挑战了“动态性必以牺牲性能为代价”的传统观念。如果成功,这将为高性能系统的开发与维护模式带来范式级的转变。
5. 启示与建议
这场对话首先挑战了一个核心假设:开发者便利性(developer convenience)与系统控制力必然是相互对立的。传统观点认为,语言越是自动管理内存、隐藏底层细节,开发者就越轻松。Kelley和Feldman的实践则暗示,真正的便利性可能来自对系统的深度理解和精确控制,因为这能消除意外的性能瓶颈和难以调试的“幽灵”问题,从而在长期提高开发效率。
针对系统软件开发者/编译器工程师:
- 重新审视数据结构设计:在性能敏感的模块中,积极尝试用“索引+存储(Index + Arena)”模式替代传统的指针链式结构。这不仅能提升缓存局部性,更能为高效序列化、快照和撤销/重做功能打开大门,其收益远不止于编译速度。
- 将可测试性作为IO设计的核心原则:在设计任何与外部世界交互的接口时,优先考虑如何通过依赖注入(即使只是传递一个简单的接口/虚函数表)使其变得可测试。Zig的IO模式是一个极佳的参考,它可以让你的核心逻辑与具体的IO实现完全解耦。
针对语言设计师与技术架构师:
- 区分“信息”与“障碍”:重新评估你的工具链中的“警告”与“错误”。Kelley关于“警告是应被修复的bug”的哲学值得深思。同时,Feldman提出的“告知但不阻塞”(Inform but don’t block)——即报告错误但仍生成可执行文件以运行部分测试——为如何在不牺牲CI流程严格性的前提下,改善“工作中”(work-in-progress)的开发体验提供了一种创新的思路。
- 在API中明确表达执行语义:Kelley转述的“并发”与“异步”的区分,提醒我们在设计并发API时,需要为用户提供表达不同强度需求(“可以乱序” vs “必须交错”)的能力。这能避免用户写出在特定调度器下会死锁的“正确”代码。
针对CTO与技术决策者:
- 理解Zig所代表的第三种技术选择:在评估Rust(静态安全)、Go(简化并发)和C++(存量生态)等主流选项之外,应将Zig视为一种独特的、面向“专家级”团队的高风险高回报选项。它要求更高的团队技能水平和更严格的开发纪律,但可能在性能、控制力和代码简洁性方面提供无与伦-比的优势。
结论强度说明:对话中关于内存布局优化带来性能提升的论述是业界公认的强信号。而Roc基于共享内存的热重载架构,以及“告知但不阻塞”的编译器哲学,目前仍处于理论和早期实践阶段,应被视为合理的推断,其普适性和长期效果有待观察。
6. 金句摘录
-
“it’s an engineering question like do you need a copy do you need an async copy of the standard library or not that’s the question for me”
意译:“这对我来说是个工程问题:你是否真的需要一个完全独立的、异步版本的标准库?这才是核心所在。”
语境:在回应社区关于Zig新IO设计是否是“函数颜色问题”的争论时,Andrew Kelley如此说道。他认为学术化的分类(如“函数颜色”)掩盖了问题的本质,即一个务实的工程权衡——与其维护两套API,不如通过参数化来统一接口。
-
“concurrency is not parallelism… he introduces a third word which is asynchrony”
意译:“并发不是并行……(Loris Cro)引入了第三个词:异步。”
语境:Kelley在解释系统编程中一个微妙但至关重要的概念。他引用Loris Cro的观点,将通常被混为一谈的“异步”和“并发”进行了精确区分,指出前者是“允许乱序”,后者是“必须交错以避免死锁”,这对编写健壮的并发代码至关重要。
-
“it’s kind of like memory allocation, isn’t it? Like it’s just function memory allocation. It’s the same problem as a memory allocator”
意译:“这不就像内存分配吗?它就是‘函数内存分配’。这和内存分配器是同一个问题。”
语境:在讨论增量链接(incremental linking)时,Kelley将“在可执行文件中为修改后的函数找到新空间并更新所有引用”这一过程,与“在内存中为对象分配空间并管理碎片”进行了类比。这个洞察揭示了不同技术领域底层模型的共通性,展现了第一性原理思维的威力。
-
“If you catch a virus with anti virus software, it is too late. Like that’s the wrong solution to the job.”
意译:“如果你需要用杀毒软件来抓病毒,那已经太晚了。这从根本上就是解决问题的错误方式。”
语境:在讨论高级开发技术(如运行时内存修改)可能被杀毒软件误报时,Kelley表达了他对安全的根本看法。他认为真正的安全来自于从源头预防(如不下载来路不明的软件),而非事后被动检测。这与他对编程语言安全的哲学一脉相承:通过好的设计和程序员的纪律从源头避免bug,而不是仅仅依赖工具做事后弥补。