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 解除堆栈时不要中止(这将恢复在 1.24.0 中添加的行为)
  • 在 Windows 上为链接器参数发出 UTF-16 文件
  • 使错误索引生成器再次工作
  • 如果需要更新,Cargo 将在 Windows 7 上发出警告。

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

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

通过 FFI 解除堆栈时不要中止

简而言之:1.24.0 中的新行为破坏了 rlua crate,并且正在恢复。如果您自此更改了代码以利用 1.24.0 中的行为,则需要暂时恢复它。虽然我们仍然计划最终引入此行为,但我们将以更慢的速度和新的实现策略来推出它。

引用 1.24 的发布公告

我们还想在此处谈论另一个变化:未定义行为。Rust 通常会努力减少未定义行为,在安全代码中不包含任何未定义行为,而在不安全代码中尽可能少地包含未定义行为。当 panic! 跨越 FFI 边界时,您可能会调用 UB。换句话说,这

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

这是行不通的,因为 panic 的确切机制必须与“C”ABI(在本例中)或其他示例中的任何其他 ABI 的工作方式相协调。

在 Rust 1.24 中,此代码现在将中止,而不是产生未定义的行为。

如上所述,这导致了破坏。它始于 针对 rlua crate 提交的错误rlua 是一个在 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 检查和借用检查对此类控制流一无所知,如果您在 Rust 堆栈帧上 longjmp,而该堆栈帧上具有任何不是 Copy 类型的类型,则会导致未定义的行为。但是,如果跳转完全发生在 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++ 异常使用 SEH,Rust panic 也是如此。

在我们整理清楚正在发生的具体细节之前,让我们先看看 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 panic 都使用 SEH,因此 longjmp 被“捕获”,并运行新的中止代码!

这里的 解决方案 是生成中止处理程序,但采用 longjmp 不会触发它的方式。目前尚不完全清楚这是否会进入 Rust 1.25;如果进展顺利,我们可能会进行反向移植,否则,此功能将在 1.26 中恢复。

在 Windows 上为链接器参数发出 UTF-16 文件

简而言之:rustc 在某些极端情况下停止了对某些 Windows 用户的工作。如果它一直在为您工作,则您不会受到此错误的影响。

与之前非常复杂且难以理解的错误相比,此错误的影响很简单:如果您在调用 rustc 的目录中具有非 ASCII 路径,则在 1.24 中,它将错误地报告类似以下消息的错误:

fatal error LNK1181: cannot open input file

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

当生成链接器时,众所周知,rustc 会超出操作系统对命令行过大的限制,尤其是在 Windows 上。对于增量编译尤其如此,因为每次编译可以有数十个目标文件。编译器目前具有用于检测生成失败并在生成失败时通过文件传递参数的逻辑,但只有在进程实际未能生成时才会触发此失败检测。

但是,在生成该文件时,我们的操作不正确。正如 文档所述

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

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

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

简而言之:在某些情况下,使用 Rust 1.24.0 构建 Rust 1.24.0 会中断。如果您没有自己构建 Rust,则您不会受到此错误的影响。

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

由于此问题不是特别有趣,并且仅影响一小部分人,他们现在应该都知道该问题,因此我们不会进一步讨论细节。要了解更多信息,请查看该问题和由此产生的讨论。

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

简而言之:如果您使用的是没有应用安全修补程序的旧 Windows,则 Cargo 无法从 crates.io 获取索引。如果您使用的是较新的 Windows 或已修补的 Windows,则您不会受到此错误的影响。

在 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。在 master 分支上,我们已经 更新以修复此问题,但对于 1.24.1 稳定版,我们 发出警告,建议他们升级 Windows 版本。尽管 libgit2 的修复程序可以被向后移植,但我们认为代码更改的影响对于此小版本来说太大了,尤其是在问题不影响已打补丁的系统的情况下。

1.24.1 的贡献者

感谢!