发布 Rust 1.24.1

2018 年 3 月 1 日 · Rust 核心团队

Rust 团队很高兴宣布新版本 Rust 1.24.1。Rust 是一种专注于安全性、速度和并发性的系统编程语言。

如果你之前通过 rustup 安装了 Rust,获取 Rust 1.24.1 非常简单,只需

$ rustup update stable

如果你还没有安装,可以从我们网站的相应页面获取 rustup,并在 GitHub 上查看 1.24.1 的详细发布说明

1.24.1 稳定版包含什么

在 1.24.0 中发现了一些小问题,这些问题共同促成了本次发布。

变更快速总结

  • 在通过 FFI unwinding 时不会中止(这恢复了 1.24.0 中添加的行为)
  • 在 Windows 上为链接器参数生成 UTF-16 文件
  • 使错误索引生成器再次工作
  • 如果需要更新,Cargo 会在 Windows 7 上发出警告。

如果你的代码可以继续构建,那么可能影响你的唯一问题是 unwinding 问题。我们计划在 1.25 或 1.26 版本中恢复此行为,具体取决于新策略的推行顺利程度。

接下来,让我们深入了解细节!

在通过 FFI unwinding 时不会中止

TL;DR:1.24.0 中的新行为破坏了 rlua crate,因此正在回滚。如果你之前修改了代码以利用 1.24.0 中的行为,现在需要将其改回。虽然我们最终仍计划引入此行为,但我们会更缓慢地推出,并采用新的实现策略。

引用1.24 发布公告

这里还有另一项我们想讨论的变更:未定义行为。Rust 通常致力于最大限度地减少未定义行为,在安全代码中杜绝,在非安全代码中也尽可能减少。在一个可能触发未定义行为的领域是当 panic! 跨越 FFI 边界时。换句话说,这

extern "C" fn panic_in_ffi() {
    panic!("Test");
}

这是不可行的,因为 panic 的具体工作机制需要与“C”ABI(在本例中,或其他例子中的任何其他 ABI)的工作方式兼容。

在 Rust 1.24 中,这段代码现在会中止,而不是产生未定义行为。

如上所述,这导致了中断。问题始于针对 rlua crate 提交的一个 bugrlua 是一个提供 Rust 和 Lua 编程语言之间高级绑定的包。

旁注:rluaChucklefish 维护,这是一家来自伦敦的游戏开发工作室,正在使用 Rust。Lua 是一种非常流行的语言,用于扩展和编写游戏脚本。我们非常重视生产环境中的 Rust 用户,因此处理这个问题是 Rust 团队的一个非常高的优先级。

在 Windows 上,而且只在 Windows 上,任何尝试处理来自 Lua 的错误都会导致中止。这使得 rlua 无法使用,因为 Lua 中的任何类型的错误都会导致你的程序终止。

深入研究后,找到了罪魁祸首:setjmp/longjmp。C 标准库提供了这些函数作为错误处理的一种手段。你先调用 setjmp,然后在稍后的某个时间点调用 longjmp。这样做时,控制流会返回到你之前调用 setjmp 的位置。这通常被用来实现异常,有时甚至用来实现协程。Lua 的实现使用了 setjmp/longjmp 来实现异常

与 C++ 或 Java 不同,C 语言不提供异常处理机制。为了解决这一困难,Lua 使用了 C 的 setjmp 工具,这产生了一种类似于异常处理的机制。(如果你用 C++ 编译 Lua,很容易修改代码使其转而使用真正的异常。)

问题是:当一些 C 代码通过 Rust 栈帧进行 setjmp/longjmp 时会发生什么?由于 drop checking 和 borrow checking 对这种控制流方式一无所知,如果你跨越一个栈上包含非 Copy 类型的 Rust 栈帧进行 longjmp,将导致未定义行为。然而,如果跳转完全发生在 C 代码中,这应该能正常工作。rlua 就是这样处理的:对 Lua 的每次调用都lua_pcall 包裹起来

然而,当你为 Lua 编写库函数时,有一种标准的方式来处理错误。每当一个 C 函数检测到错误时,它只需调用 lua_error,(或者更好的是 luaL_error,它格式化错误消息然后调用 lua_error)。lua_error 函数会清除 Lua 中需要清除的一切,并跳回到发起该执行的 lua_pcall,同时传递错误消息。

那么问题来了:为什么会中断?又为什么只在 Windows 上中断?

最初讨论 setjmp/longjmp 时,这里有一句关键短语没有突出显示。那就是

深入研究后,找到了罪魁祸首:setjmp/longjmp。这些函数是作为错误处理的一种手段,由C 标准库提供的。

这些函数不是 C 语言的一部分,而是标准库的一部分。这意味着平台开发者会实现这些函数,而他们的实现可能有所不同。

Windows 有一个称为 SEH 的概念,是“结构化异常处理”的缩写。Windows 使用 SEH 来实现 setjmp/longjmp,因为 SEH 的核心思想就是统一错误处理。出于类似的原因,C++ 异常和 Rust 的 panic 也使用了 SEH。

在我们弄清具体发生了什么之前,先看看 rlua 是如何工作的。rlua 有一个内部函数 protect_lua_call,用于调用 Lua。它的使用方式如下:

protect_lua_call(self.state, 0, 1, |state| {
    ffi::lua_newtable(state);
})?;

也就是说,protect_lua_call 接受一些参数,其中一个是闭包。这个闭包被传递给 lua_pcall,它会捕获由传递给它的代码(也就是那个闭包)可能抛出的任何 longjmp

考虑上面的代码,想象一下这里的 lua_newtable 可能会调用 longjmp。应该发生的情况是:

  1. protect_lua_call 接受我们的闭包,并将其传递给 lua_pcall
  2. lua_pcall 调用 setjmp 来处理任何错误,并调用我们的闭包。
  3. 在我们的闭包内部,lua_newtable 发生错误,并调用 longjmp
  4. 最初的 lua_pcall 用它之前调用的 setjmp 捕获了 longjmp
  5. 一切正常。

然而,protect_lua_call 的实现将我们的闭包转换为了 extern fn,因为 Lua 需要这种形式。因此,在 1.24.0 的变更下,它设置了一个 panic 处理程序,会导致中止。换句话说,现在的代码大致看起来像这样的伪代码:

protect_lua_call(self.state, 0, 1, |state| {
    let result = panic::catch_unwind(|| {
        ffi::lua_newtable(state);
    });

    if result.is_err() {
        process::abort();
    }
})?;

早些时候,在讨论 setjmp/longjmp 时,我们提到它在 Rust 代码中的问题在于它无法处理 Rust 的析构函数。因此,在除 Windows 之外的所有平台上,上述 catch_unwind 的小把戏实际上是被忽略的,所以一切正常。然而,在 Windows 上,由于 setjmp/longjmp 和 Rust panics 都使用 SEH,longjmp 会被“捕获”,并运行新的中止代码!

的解决方案是生成中止处理程序,但要以 longjmp 不会触发它的方式。目前尚不确定这是否会进入 Rust 1.25;如果进展顺利,我们可能会回移植,否则,此功能将在 1.26 版本中恢复。

在 Windows 上为链接器参数生成 UTF-16 文件

TL;DR:在某些边缘情况下,rustc 对一些 Windows 用户停止工作。如果它对你一直正常,你则没有受到此 bug 的影响。

与前一个非常复杂且难以理解的 bug 相反,这个 bug 的影响很简单:如果你在调用 rustc 的目录中包含非 ASCII 路径,在 1.24 中,它会错误地报告如下消息:

fatal error LNK1181: cannot open input file

导致此问题的 PR,#47507,对最终导致问题的行为有一个很好的解释:

在启动链接器时,rustc 历史上已知会超出操作系统的命令行长度限制,尤其是在 Windows 上。对于增量编译来说更是如此,每次编译可能会有几十个目标文件。编译器目前有逻辑检测启动失败,然后通过文件而不是直接传递参数,但这种失败检测只有在进程实际启动失败时才会触发。

然而,在生成该文件时,我们做得不正确。正如文档所述

响应文件和 DEF 文件可以是带 BOM 的 UTF-16,也可以是 ANSI。

我们提供的是一个 UTF-8 编码的文件,不带 BOM。因此,修复方法很简单:生成一个带 BOM 的 UTF-16 文件。

使错误索引生成器再次工作

TL;DR:在某些情况下,使用 Rust 1.24.0 构建 Rust 1.24.0 会失败。如果你自己不构建 Rust,则未受此 bug 影响。

在为各种 Linux 发行版打包 Rust 时,发现使用 1.24 构建 1.24 会失败。这是由于路径不正确导致的,使得某些元数据未能正确生成。

由于这个问题不是特别有趣,且只影响一小部分人(他们现在应该都已经意识到这个问题了),我们将不再深入讨论细节。要了解更多信息,请查看该问题和由此产生的讨论。

如果需要更新,Cargo 会在 Windows 7 上发出警告。

TL;DR:如果你使用的是没有应用安全补丁的旧版本 Windows,Cargo 可能无法从 crates.io 获取索引。如果你使用的是较新的 Windows 或已打补丁的 Windows,则未受此 bug 影响。

2017 年 2 月,GitHub 宣布将停止支持弱加密标准。一年后,即 2018 年 2 月,弃用期结束,支持已移除。总的来说,这是一件好事。

Cargo 使用 GitHub 存储 Crates.io 的索引,这是我们的包仓库。它还使用 libgit2 进行 git 操作。libgit2 使用 WinHTTP 进行 HTTP 调用。作为操作系统的一部分,其功能集取决于你使用的操作系统。

本节中使用“Windows 7”代指“Windows 7、Windows Server 2018 和 Windows Server 2012”,因为这样写更短。然而,以下内容适用于这三个版本的 Windows。

Windows 7 在 2016 年 6 月收到了关于 TLS 的更新。在打补丁之前,Windows 7 默认使用 TLS 1.0。此更新允许应用程序本地使用 TLS 1.1 或 1.2。

如果你的系统没有收到该更新,那么你仍然会使用 TLS 1.0。这意味着访问 GitHub 将开始失败。

libgit2 创建了一个修复程序,使用 WinHTTP API 请求 TLS 1.2。在主分支上,我们已更新以修复此问题,但在 1.24.1 稳定版中,我们发出警告,建议用户升级其 Windows 版本。尽管 libgit2 的修复程序可以回移植,但我们认为代码更改量对于小版本发布来说太大了,特别是考虑到此问题不影响已打补丁的系统。

1.24.1 的贡献者

感谢!