Rust RFCs - Active RFC List

注意:這是官方 rust rfcs 的 fork。如果你有興趣協助翻譯的話,歡迎前往 CONTRIBUTING.md 瞭解更多細節。

Note: This is a fork of official rust rfcs. If you are interested in help translating, please have a look at CONTRIBUTING.md.

Many changes, including bug fixes and documentation improvements can be implemented and reviewed via the normal GitHub pull request workflow.

Some changes though are "substantial", and we ask that these be put through a bit of a design process and produce a consensus among the Rust community and the sub-teams.

The "RFC" (request for comments) process is intended to provide a consistent and controlled path for new features to enter the language and standard libraries, so that all stakeholders can be confident about the direction the language is evolving in.

Table of Contents

When you need to follow this process

You need to follow this process if you intend to make "substantial" changes to Rust, Cargo, Crates.io, or the RFC process itself. What constitutes a "substantial" change is evolving based on community norms and varies depending on what part of the ecosystem you are proposing to change, but may include the following.

  • Any semantic or syntactic change to the language that is not a bugfix.
  • Removing language features, including those that are feature-gated.
  • Changes to the interface between the compiler and libraries, including lang items and intrinsics.
  • Additions to std.

Some changes do not require an RFC:

  • Rephrasing, reorganizing, refactoring, or otherwise "changing shape does not change meaning".
  • Additions that strictly improve objective, numerical quality criteria (warning removal, speedup, better platform coverage, more parallelism, trap more errors, etc.)
  • Additions only likely to be noticed by other developers-of-rust, invisible to users-of-rust.

If you submit a pull request to implement a new feature without going through the RFC process, it may be closed with a polite request to submit an RFC first.

Sub-team specific guidelines

For more details on when an RFC is required for the following areas, please see the Rust community's sub-team specific guidelines for:

Before creating an RFC

A hastily-proposed RFC can hurt its chances of acceptance. Low quality proposals, proposals for previously-rejected features, or those that don't fit into the near-term roadmap, may be quickly rejected, which can be demotivating for the unprepared contributor. Laying some groundwork ahead of the RFC can make the process smoother.

Although there is no single way to prepare for submitting an RFC, it is generally a good idea to pursue feedback from other project developers beforehand, to ascertain that the RFC may be desirable; having a consistent impact on the project requires concerted effort toward consensus-building.

The most common preparations for writing and submitting an RFC include talking the idea over on our official Discord server, discussing the topic on our developer discussion forum, and occasionally posting "pre-RFCs" on the developer forum. You may file issues on this repo for discussion, but these are not actively looked at by the teams.

As a rule of thumb, receiving encouraging feedback from long-standing project developers, and particularly members of the relevant sub-team is a good indication that the RFC is worth pursuing.

What the process is

In short, to get a major feature added to Rust, one must first get the RFC merged into the RFC repository as a markdown file. At that point the RFC is "active" and may be implemented with the goal of eventual inclusion into Rust.

  • Fork the RFC repo RFC repository
  • Copy 0000-template.md to text/0000-my-feature.md (where "my-feature" is descriptive). Don't assign an RFC number yet; This is going to be the PR number and we'll rename the file accordingly if the RFC is accepted.
  • Fill in the RFC. Put care into the details: RFCs that do not present convincing motivation, demonstrate lack of understanding of the design's impact, or are disingenuous about the drawbacks or alternatives tend to be poorly-received.
  • Submit a pull request. As a pull request the RFC will receive design feedback from the larger community, and the author should be prepared to revise it in response.
  • Each pull request will be labeled with the most relevant sub-team, which will lead to its being triaged by that team in a future meeting and assigned to a member of the subteam.
  • Build consensus and integrate feedback. RFCs that have broad support are much more likely to make progress than those that don't receive any comments. Feel free to reach out to the RFC assignee in particular to get help identifying stakeholders and obstacles.
  • The sub-team will discuss the RFC pull request, as much as possible in the comment thread of the pull request itself. Offline discussion will be summarized on the pull request comment thread.
  • RFCs rarely go through this process unchanged, especially as alternatives and drawbacks are shown. You can make edits, big and small, to the RFC to clarify or change the design, but make changes as new commits to the pull request, and leave a comment on the pull request explaining your changes. Specifically, do not squash or rebase commits after they are visible on the pull request.
  • At some point, a member of the subteam will propose a "motion for final comment period" (FCP), along with a disposition for the RFC (merge, close, or postpone).
    • This step is taken when enough of the tradeoffs have been discussed that the subteam is in a position to make a decision. That does not require consensus amongst all participants in the RFC thread (which is usually impossible). However, the argument supporting the disposition on the RFC needs to have already been clearly articulated, and there should not be a strong consensus against that position outside of the subteam. Subteam members use their best judgment in taking this step, and the FCP itself ensures there is ample time and notification for stakeholders to push back if it is made prematurely.
    • For RFCs with lengthy discussion, the motion to FCP is usually preceded by a summary comment trying to lay out the current state of the discussion and major tradeoffs/points of disagreement.
    • Before actually entering FCP, all members of the subteam must sign off; this is often the point at which many subteam members first review the RFC in full depth.
  • The FCP lasts ten calendar days, so that it is open for at least 5 business days. It is also advertised widely, e.g. in This Week in Rust. This way all stakeholders have a chance to lodge any final objections before a decision is reached.
  • In most cases, the FCP period is quiet, and the RFC is either merged or closed. However, sometimes substantial new arguments or ideas are raised, the FCP is canceled, and the RFC goes back into development mode.

The RFC life-cycle

Once an RFC becomes "active" then authors may implement it and submit the feature as a pull request to the Rust repo. Being "active" is not a rubber stamp, and in particular still does not mean the feature will ultimately be merged; it does mean that in principle all the major stakeholders have agreed to the feature and are amenable to merging it.

Furthermore, the fact that a given RFC has been accepted and is "active" implies nothing about what priority is assigned to its implementation, nor does it imply anything about whether a Rust developer has been assigned the task of implementing the feature. While it is not necessary that the author of the RFC also write the implementation, it is by far the most effective way to see an RFC through to completion: authors should not expect that other project developers will take on responsibility for implementing their accepted feature.

Modifications to "active" RFCs can be done in follow-up pull requests. We strive to write each RFC in a manner that it will reflect the final design of the feature; but the nature of the process means that we cannot expect every merged RFC to actually reflect what the end result will be at the time of the next major release.

In general, once accepted, RFCs should not be substantially changed. Only very minor changes should be submitted as amendments. More substantial changes should be new RFCs, with a note added to the original RFC. Exactly what counts as a "very minor change" is up to the sub-team to decide; check Sub-team specific guidelines for more details.

Reviewing RFCs

While the RFC pull request is up, the sub-team may schedule meetings with the author and/or relevant stakeholders to discuss the issues in greater detail, and in some cases the topic may be discussed at a sub-team meeting. In either case a summary from the meeting will be posted back to the RFC pull request.

A sub-team makes final decisions about RFCs after the benefits and drawbacks are well understood. These decisions can be made at any time, but the sub-team will regularly issue decisions. When a decision is made, the RFC pull request will either be merged or closed. In either case, if the reasoning is not clear from the discussion in thread, the sub-team will add a comment describing the rationale for the decision.

Implementing an RFC

Some accepted RFCs represent vital features that need to be implemented right away. Other accepted RFCs can represent features that can wait until some arbitrary developer feels like doing the work. Every accepted RFC has an associated issue tracking its implementation in the Rust repository; thus that associated issue can be assigned a priority via the triage process that the team uses for all issues in the Rust repository.

The author of an RFC is not obligated to implement it. Of course, the RFC author (like any other developer) is welcome to post an implementation for review after the RFC has been accepted.

If you are interested in working on the implementation for an "active" RFC, but cannot determine if someone else is already working on it, feel free to ask (e.g. by leaving a comment on the associated issue).

RFC Postponement

Some RFC pull requests are tagged with the "postponed" label when they are closed (as part of the rejection process). An RFC closed with "postponed" is marked as such because we want neither to think about evaluating the proposal nor about implementing the described feature until some time in the future, and we believe that we can afford to wait until then to do so. Historically, "postponed" was used to postpone features until after 1.0. Postponed pull requests may be re-opened when the time is right. We don't have any formal process for that, you should ask members of the relevant sub-team.

Usually an RFC pull request marked as "postponed" has already passed an informal first round of evaluation, namely the round of "do we think we would ever possibly consider making this change, as outlined in the RFC pull request, or some semi-obvious variation of it." (When the answer to the latter question is "no", then the appropriate response is to close the RFC, not postpone it.)

Help this is all too informal!

The process is intended to be as lightweight as reasonable for the present circumstances. As usual, we are trying to let the process be driven by consensus and community norms, not impose more structure than necessary.

License

This repository is currently in the process of being licensed under either of:

  • Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
  • MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)

at your option. Some parts of the repository are already licensed according to those terms. For more see RFC 2044 and its tracking issue.

Contributions

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

概要

允許屬性在配對分支上

動機

有時希望使用屬性來注釋配對語句的分支,例如條件編譯 #[cfg] 或是分支權重(後者為最重要的用途)。

對於條件編譯,暫時得解決辦法是使用 #[cfg] 來重複宣告相同函式來處理不同的案例。一個案例研究是 sfackler 的 bindings to OpenSSL,在多數的發行版本中移除了 SSLv2 支持,因此 Rust bindings 部分需要被條件禁用。支援各種不同 SSL 版本最顯而易見的方法是使用枚舉。

#![allow(unused)]
fn main() {
pub enum SslMethod {
    #[cfg(sslv2)]
    /// Only support the SSLv2 protocol
    Sslv2,
    /// Only support the SSLv3 protocol
    Sslv3,
    /// Only support the TLSv1 protocol
    Tlsv1,
    /// Support the SSLv2, SSLv3 and TLSv1 protocols
    Sslv23,
}
}

然而,所有的 match 只能在 cfg 啟用時使用 Sslv2,例如下面內容是無效的:

#![allow(unused)]
fn main() {
fn name(method: SslMethod) -> &'static str {
    match method {
        Sslv2 => "SSLv2",
        Sslv3 => "SSLv3",
        _ => "..."
    }
}
}

一個有效的方法則必須有兩個定義:#[cfg(sslv2)] fn name(...)#[cfg(not(sslv2)] fn name(...) 。前者有 Sslv2 的分支,後者沒有。顯然,對於枚舉中每個額外的 cfg 變體,這都會以指數型的方式爆炸。

分支權重將允許仔細的微優化器(micro-optimiser)通知編譯器,例如,鮮少採取某個配對分支:

#![allow(unused)]
fn main() {
match foo {
    Common => {}
    #[cold]
    Rare => {}
}
}

詳細設計

一般的屬性語法,應用於整個配對分支。

#![allow(unused)]
fn main() {
match x {
    #[attr]
    Thing => {}

    #[attr]
    Foo | Bar => {}

    #[attr]
    _ => {}
}
}

替代方案

實際上沒有通用的替代方案; 人們也許可以用一些巨集(macro)和輔助函數來解決條件枚舉變體的配對問題; 但一般來說,這起不了任何作用。

未解決的問題

概要

此 RFC 增加了多載切片符號

  • foo[] 對應 foo.as_slice()
  • foo[n..m] 對應 foo.slice(n, m)
  • foo[n..] 對應 foo.slice_from(n)
  • foo[..m] 對應 foo.slice_to(m)
  • 以上所有的 mut 變體

透過SliceSliceMut 兩個新特徵(trait)。

它還將 範圍配對模式的符號改為 ...,以表示它們是包含的,而 .. 在切片中是不包含的。

動機

引入此功能有兩個主要動機。

人因工程

在處理 vector 或其他類型的容器時,切片操作(特別是 as_slice)是相當常見的基本操作。我們已經有了透過 Index 特徵進行索引的語法,此 RFC 本質上是這特徵的延續。

as_slice 運算子尤其重要,自從我們已經擺脫以強迫的方式自動切片,明確地呼叫 as_slice 變得非常普遍,這也是語言中人因工程優先/第一印象的問題之一。還有一些其他方法可以解決這個特定問題,但這些替代方法具有下方討論的一些缺點(參閱「替代方案」)。

錯誤處理協定

我們正在逐漸走向一個類似 Python 的世界,像是當 n 超出範圍時,foo[n] 會呼叫 fail!,而相應的方法像是 get 則返回 Option 而不是失敗。透過為切片提供類似的符號,我們打開了在整個 vector-like APIs 中遵循相同協定的大門。

詳細設計

此設計是對 Index 特徵設計的直接延續。我們引入了兩個新特徵(trait),用於不可變與可變切片:

#![allow(unused)]
fn main() {
trait Slice<Idx, S> {
    fn as_slice<'a>(&'a self) -> &'a S;
    fn slice_from(&'a self, from: Idx) -> &'a S;
    fn slice_to(&'a self, to: Idx) -> &'a S;
    fn slice(&'a self, from: Idx, to: Idx) -> &'a S;
}

trait SliceMut<Idx, S> {
    fn as_mut_slice<'a>(&'a mut self) -> &'a mut S;
    fn slice_from_mut(&'a mut self, from: Idx) -> &'a mut S;
    fn slice_to_mut(&'a mut self, to: Idx) -> &'a mut S;
    fn slice_mut(&'a mut self, from: Idx, to: Idx) -> &'a mut S;
}
}

(請注意,此處的可變名稱是命名規則可能更改的一部分,將在單獨的 RFC 中描述)。

這些特徵將在解釋以下符號時使用:

不可變切片

  • foo[] 對應 foo.as_slice()
  • foo[n..m] 對應 foo.slice(n, m)
  • foo[n..] 對應 foo.slice_from(n)
  • foo[..m] 對應 foo.slice_to(m)

可變切片

  • foo[mut] 對應 foo.as_mut_slice()
  • foo[mut n..m] 對應 foo.slice_mut(n, m)
  • foo[mut n..] 對應 foo.slice_from_mut(n)
  • foo[mut ..m] 對應 foo.slice_to_mut(m)

Index 一樣,這種表示法的使用將自動取值,就如同對應的方法調用一樣。因此,如果 T 實現了 Slice<uint, [U]>s: Smaht<T>,那麼 s[] 就可以編譯並具有 &[U] 類型。

請注意,切片是「不包含」(因此 [n..m] 是等於 n <= x < m),而模式配對中的 .. 是「包含」。為了避免混淆,我們建議將配對符號更改為 ... 來做出區別。更改符號而不是解釋的原因是,「不包含」(分別「包含」)解釋是切片(相較於「配對」)的正確默認值。

使用此語法的基本理由

用於切片的方括號的選擇很簡單:它與我們的索引表示法契合,並且切片和索引密切相關。

其他一些語言(如 Python 和 Go 和 Fortran)在切片表示法中使用 : 而不是 ..。在 Rust 中,.. 的選擇受到 Rust 本身其他地方的使用影響,例如固定長度陣列類型 [T, ..n]。用於切片的 .. 在 Perl 和 D 中有先例。

有關程式語言中切片表示法歷史的更多資訊,請參閱 維基百科

mut 限定符

可能令人驚訝的是,mut 在建議的切片表示法中作為限定符,而不是用於索引表示法。原因是索引包含隱式取消引用。假如 v: Vec<Foo>v[n] 的類型為 Foo,而不是 &Foo&mut Foo。因此,如果你想透過索引取得可變引用,你可以寫成 &mut v[n]。更廣泛地說,這允許我們在解決可變性之前進行解析/類型檢查。

這種對 Index 的處理符合 C 的傳統,並允許我們寫成 v[0] = foo,而不是 *v[0] = foo

另一方面,這種方法對於切片是有問題的,因為一般來說,他會產生一個 unsized 類型(在 DST 中),當然,切片是為了給你一個指向切片大小的胖指標,我們不希望立即取消引用。但是,這樣的結果是,我們需要預先知道切片的可變性,因為它決定了表達式的類型。

缺點

主要的缺點是增加了語言語法的複雜度。這看起來微不足道,尤其是因為這裡的符號本質上是 "完成" 以 Index 特徵開始的內容。

設計上的限制

Index 特徵一樣,這迫使結果透過 成為一個引用,這可能會排除切片的普遍化。

解決此問題的一個方法是切片方法使用 self (依值) 而不是 &self,反過來在 &T 而不是 T 上實現特徵。這個方法是否長期可行將取決於方法解析和自動引用的最終規則。

一般來說,當特徵可以應用於類型 T 而不是借用類型 &T 時,特徵系統運作的最好。最終,如果 Rust 獲得了更高的類型(HKT),我們可以將特徵中的切片類型 S 更改為更高的類型,這樣他就是一個由生命週期索引的類型家族。然後,我們可以用 S<'a> 來代替返回值中的 &'a S。在未來,我們應該可以從當前的 IndexSlice 特徵設計轉換到一個 HKT 版本,而不會破壞相後的兼容性,方法是對實現舊特徵的類型使用新特徵的全面實現(例如 IndexHKT)。

替代方案

對於改善 as_slice 的易用性,有兩個主要替代方案。

強制: 自動切片

一種可能性是重新引入某種自動切片的強制。我們曾經有一個從(用現今的話來說)Vec<T>&[T] 的強制轉換。由於我們不再強制擁有借來的值,我們現在可能想要一個強制 &Vec<T>&[T] 的轉換:

#![allow(unused)]
fn main() {
fn use_slice(t: &[u8]) { ... }

let v = vec!(0u8, 1, 2);
use_slice(&v)           // automatically coerce here
use_slice(v.as_slice()) // equivalent
}

不幸的是,添加這樣的強制需要在以下選項中進行選擇:

  • 將強制與 VecString 連繫起來,這將重新引入對這些原本純粹的函式庫類型的特殊處理,並且意味著其他支援切片的函數庫類型不會受益(違背了一些 DST 的目的)。

  • 透過特徵使強制可擴展。然而這是在打開潘朵拉的盒子:這個機制很可能被用來在強制的過程中運行任意的程式碼,所以任何呼叫 foo(a, b, c) 都可能涉及運行程式碼來預處理每個參數。雖然我們最終可能想要這種使用者可擴展的強制機制,但在理解程式碼時,這是一個很的步驟,有很多潛在的不利因素,所以我們應該先追求更保守的解決方案。

解引用

另一種可能是讓 String 實現 Deref<str>Vec<T> 實現 Deref<[T]> 一旦 DST 登陸。這樣做將允許顯式的強制機制,例如:

#![allow(unused)]
fn main() {
fn use_slice(t: &[u8]) { ... }

let v = vec!(0u8, 1, 2);
use_slice(&*v)          // take advantage of deref
use_slice(v.as_slice()) // equivalent
}

但是,這樣做至少有兩個缺點:

  • 目前不清楚方法解析規則最終將如何與 Deref 互動。特別是,一個領先的提議是,對於一個智慧指標 s: Smaht<T> ,當你呼叫 s.m(...) 時,對於 Smaht<T> 只有 內置 方法 m 被考慮到;僅考慮最大引用值 *s 的特徵方法。

    使用這樣的解析策略,為 Vec 實現 Deref 將使得我們無法在 Vec 類型上使用特徵方法 ,除非透過 UFCS,這嚴重限制了程式設計師為 Vec 有效地實現新特徵的能力。

  • Vec 作為一個圍繞切片的智慧指標的想法,以及如上所述的 &*v 的使用,有點違反直覺,特別是對於這樣的基本類型。

追根究底,無論如何,切片的符號本身似乎是可取的,如果它能消除對 VecString 實現 Deref 的需要,那就更好了。

總結

此 RFC 透過添加 global_asm! 巨集(macro)公開了 LLVM 對 module-level inline assembly 的支援。語法非常簡單:它只需要一個包含組合語言代碼的字串字面值。

範例:

#![allow(unused)]
fn main() {
global_asm!(r#"
.globl my_asm_func
my_asm_func:
    ret
"#);

extern {
    fn my_asm_func();
}
}

動機

此功能有兩個主要使用案例。首先,它允許完全在組合語言中寫入函式,這大大消除了對 naked 屬性的需要。這主要適用於使用自訂呼叫規則(如中斷處置器)的函式。

另一個重要的使用案例是,它允許外部組合語言檔案在 Rust 模組中使用,而無需駭入編譯系統。

#![allow(unused)]
fn main() {
global_asm!(include_str!("my_asm_file.s"));
}

組合語言檔案也可以由 build.rs 預處理或產生(例如使用 C 預處理器),這將在 Cargo 輸出目錄中產生輸出檔案:

#![allow(unused)]
fn main() {
global_asm!(include_str!(concat!(env!("OUT_DIR"), "/preprocessed_asm.s")));
}

詳細設計

見上文所述,沒有要多補充的。巨集(macro)會直接映射到 LLVM 的 module asm

缺點

asm!一樣,這個功能取決於 LLVM 的集成組合語言

替代方案

包含外部組合語言的現有方式是使用 build.rs 中的 gcc 編譯組合語言檔案,並將其連結到 Rust 程式中作為靜態庫。

對於完全以組合語言寫入的函式的替代方案是添加一個 #[naked] function attribute.

未解決問題

總結

讓型別建構子(type constructor)能與特徵(trait)相互關聯。在 Rust 使用者最想要的功能中,有個通常稱為「高階種類型別(higher-kinded types)」的功能,而這是達成這項目標的其中一步。關聯型別建構子(associated type constructors)這項特定的功能可以解決高階種類其中一項最普遍的使用情境,比起其他形式的高階種類多型(polymorphism)更容易擴充至原有的型別系統,而且能兼容未來可能所導入的更複雜形式的高階種類多型。

動機

讓我們先考慮以下的特徵範例來展示可能的動機:

#![allow(unused)]
fn main() {
trait StreamingIterator {
    type Item<'a>;
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
}

這樣的特徵是很實用的,它能提供一種疊代器(Iterator)所回傳的數值能附有一個生命週期,且能與傳至 next 的引用所擁有的生命週期綁定。此特徵最明顯的使用情境就是對一個向量(vector)所產生的疊代器,在每次疊代時都能產生重疊(overlapping)且可變(mutable)的子切片(subslice)。使用標準的 Iterator 介面的話,這樣的實作是無效的,因為每個切片必須活得與整個疊代器一樣長,而非只是透過 next 借用的時間而已。

這樣的特徵無法用現在的 Rust 表現出來,因為它需要依賴某種形式的高階種類多型。本 RFC 將擴充 Rust 來確保此特定形式的高階種類多型,也就是我們在此所稱作的關聯型別建構子。此功能有各種應用場景,但最主要的應用都還是與 StreamingIterator 類似:定義的特徵所持有的型別能附有一個生命週期,並與接收型別所借用的生命週期相互關聯。

設計細節

背景:什麼是種類(Kind)?

「高階種類型別」算是比較籠統的詞彙,容易與其他語言功能混淆,而造成我們想表達的概念不精確。所以本 RFC 將簡單介紹種類的概念。種類通常被稱為「型別的型別」,但這樣的解釋對於想理解的人幫助並不大,反而對已經了解這些概念的人才說得通。所以讓我們嘗試用型別比喻來了解種類。

在型別完善的語言中,每個表達式(expression)都有個型別。許多表達式都有所謂的「基本型別」,也就是語言中的原生型別而且無法用其他型別表達。在 Rust 中,booli64usizechar 都是基本型別的明顯例子。另一方面,也就會有型別會是由其他型別組成,函式就是屬於這樣的例子。讓我們看看下面這個簡單的函式:

#![allow(unused)]
fn main() {
fn not(x: bool) -> bool {
   !x
}
}

not 的型別為 bool -> bool(抱歉這邊開始會使用些與 Rust 不大一樣的語法),而 not(true) 的型別則是 bool。注意這樣的不同點正是了解高階種類的關鍵。

在種類的分析中,boolcharbool -> bool 等等型別都有 type 這種種類。每個型別都有 type 種類。然而 type 只是基本種類,就像 bool 只是個基本型別,我們能寫出更複雜的種類像是 type -> type,這樣的種類其中一個範例就是 Vec 能接受一個型別然後產生出一個型別。而 Vec 的種類與 Vec<i32> 的種類(也就是 type)之間的差異,就相對應於 notnot(true) 之間的差異。另外注意 Vec<T> 的種類和 Vec<i32> 一樣仍然是 type。盡管 T 是個型別參數,Vec<T> 還是接收了一個型別,就像 not(x) 的型別仍然是 bool,盡管 x 是個變數一樣。

Rust 相對不尋常的特色是它有兩種基本種類,相較於許多語言處理高階種類時只會有一種種類 type。Rust 的另一個基本種類就是生命週期參數。如果你有個型別像是 Foo<'a>Foo 的種類就是 lifetime -> type

而高階種類當然也可以接受多重引數,Result 的種類就是 type, type -> type,而 vec::Iter<'a, T> 中的 vec::Iter 則會有 lifetime, type -> type 這樣的種類。

高階種類的表達通常就稱為「型別運算子(type operators)」,運算出一個型別的型別運算子就稱作「型別建構子(type constructors)」。也有其他型別運算子能運算出其他型別運算子,而且還有更高階的型別運算子能接收型別運算子作為引數,讓它們的種類變成像是 (type -> type) -> type。本 RFC 將不會討論這些更進階的範疇。

本 RFC 更明確的目標就是要讓型別建構子可以與特徵相互關聯,就像你現在的特徵能與函式、型別與 consts 相互關聯一樣。型別建構子還有包含其他形式的多型,像是對型別建構子實作特徵,而非只是型別而已。但這些都不包含在本 RFC 內。

關聯型別建構子的功能

宣告與賦值給關聯型別建構子

本 RFC 將提供個非常簡單的語法來定義關聯型別建構子,它會與建立型別建構子的別名用的語法非常相似。使用此語法的目的就是為了避免讓尚未能理解高階種類的使用者使用困難。

#![allow(unused)]
fn main() {
trait StreamingIterator {
   type Item<'a>;
}
}

這樣能清楚表達關聯項目 Item 是一個型別建構子,而非只是個型別,因為它有個型別參數附加在旁。

與關聯型別一樣,關聯型別建構子可以用在界限內:

#![allow(unused)]
fn main() {
trait Iterable {
    type Item<'a>;
    type Iter<'a>: Iterator<Item = Self::Item<'a>>;
    
    fn iter<'a>(&'a self) -> Self::Iter<'a>;
}
}

此界限套用到型別建構子的「輸出」,然後參數會被視為高階參數。也就是說上述的界限幾乎等於對特徵加上這樣的界限:

#![allow(unused)]
fn main() {
for<'a> Self::Iter<'a>: Iterator<Item = Self::Item<'a>>
}

impl 中賦值給關聯型別建構子的語法與賦值給關聯型別的非常接近:

#![allow(unused)]
fn main() {
impl<T> StreamingIterator for StreamIterMut<T> {
    type Item<'a> = &'a mut [T];
    ...
}
}

使用關聯型別建構子建立型別

一旦特徵擁有關聯型別建構子時,它就能套用在作用域中的任何參數或具體項。這能同時用在特徵的本體內與本體外,使用的語法就與使用關聯型別類似。以下是一些範例:

#![allow(unused)]
fn main() {
trait StreamingIterator {
    type Item<'a>;
    // 在特徵內將生命週期 `'a` 套用至 `Self::Item`。
    fn next<'a>(&'a self) -> Option<Self::Item<'a>>;
}

struct Foo<T: StreamingIterator> {
    // 在特徵外將實際生命週期套用至建構子。
    bar: <T as StreamingIterator>::Item<'static>;
}
}

關聯型別建構子也能用來產生其他型別建構子:

#![allow(unused)]
fn main() {
trait Foo {
    type Bar<'a, 'b>;
}

trait Baz {
    type Quux<'a>;
}

impl<T> Baz for T where T: Foo {
    type Quux<'a> = <T as Foo>::Bar<'a, 'static>;
}
}

最後,關聯型別建構子的生命週期和其他型別建構子的一樣能被省略。加上生命週期省略的話,StreamingIterator 的完整定義就可以是:

#![allow(unused)]
fn main() {
trait StreamingIterator {
    type Item<'a>;
    fn next(&mut self) -> Option<Self::Item>;
}
}

在界限中使用關聯型別建構子

對於參數的型別是由特徵的關聯型別建構子產生的話,使用者可以用高階特徵界限(Higher-Rank Trait Bounds)來綁定。無論是對型別相等的界限或是對此種類的特徵界限都是有效的:

#![allow(unused)]
fn main() {
fn foo<T: for<'a> StreamingIterator<Item<'a>=&'a [i32]>>(iter: T) { ... }

fn foo<T>(iter: T) where T: StreamingIterator, for<'a> T::Item<'a>: Display { ... }
}

本 RFC 並沒有提出任何允許型別建構子自己綁定的形式,無論是相等界限或特徵界限(當然特徵界限是不可能的)。

型別引數的關聯型別建構子

本 RFC 目前所有的範例都專注在生命週期引數的關聯型別建構子。但是本 RFC 一樣有提出導入型別的關聯型別建構子:

#![allow(unused)]
fn main() {
trait Foo {
    type Bar<T>;
}
}

本 RFC 並沒有打算讓高階特徵界限能進一步接收型別引數,因為這會讓它們更難清楚地表達語義。我們的確有這樣的擴充需求,但這超出本 RFC 的範疇。

型別引數可以透過「Family」模式來達成其他形式的高階種類多型。舉例來說,使用 PointerFamily 特徵可以將 Arc 與 Rc 抽象出來:

#![allow(unused)]
fn main() {
trait PointerFamily {
    type Pointer<T>: Deref<Target = T>;
    fn new<T>(value: T) -> Self::Pointer<T>;
}

struct ArcFamily;

impl PointerFamily for ArcFamily {
    type Pointer<T> = Arc<T>;
    fn new<T>(value: T) -> Self::Pointer<T> {
        Arc::new(value)
    }
}

struct RcFamily;

impl PointerFamily for RcFamily {
    type Pointer<T> = Rc<T>;
    fn new<T>(value: T) -> Self::Pointer<T> {
        Rc::new(value)
    }
}

struct Foo<P: PointerFamily> {
    bar: P::Pointer<String>,
}
}

特徵界限與 where

關聯型別建構子的界限

關聯型別建構子的界限會被視為特徵自己的高階界限。這讓它們的行為能與一般關聯型別的界限一致。舉例來說:

#![allow(unused)]
fn main() {
trait Foo {
    type Assoc<'a>: Trait<'a>;
}
}

就等同於:

#![allow(unused)]
fn main() {
trait Foo where for<'a> Self::Assoc<'a>: Trait<'a> {
    type Assoc<'a>;
}
}

關聯型別上的 where

另一方面,在關聯型別上使用 where 則會要求每次使用關聯型別時都得滿足其限制。舉例來說:

#![allow(unused)]
fn main() {
trait Foo {
    type Assoc where Self: Sized;
}
}

每次呼叫 <T as Foo>::Assoc 時都需要證明 T: Sized,就像其他場合需要證明有符合 impl 的界限一樣。

(@nikomatsakis 相信在某些場合的 where 會需要關聯型別建構子指明處理生命週期。實際細節將不會包含在本 RFC,因為這在實際實作時才會更清楚。)

在其他高階種類多型之前只實作此功能的優點

此功能並非完整的高階種類多型,也無法允許那些在 Haskell 中熱門的抽象形式,但它能提供 Rust 獨一無二的高階種類多型使用情境。像是 StreamingIterator 與各式集合的特徵。這很可能也會是許多使用者最需要用到的功能,讓還不知道高階種類的人也能輕易直觀地使用。

此功能會有些麻煩的實作挑戰,但也避免了其他所有高階種類多型所需的功能像是:

  • 定義高階種類特徵
  • 對型別運算子實作高階種類
  • 高階型別運算子
  • 高階種類特徵綁定的型別運算子參數
  • 型別運算子參數套用至一個給予的型別或型別參數

建議語法的優點

建議語法的優點在於它建立在已經存在的語法上,型別建構子已經能在 Rust 中使用相同的語法建立別名。雖然型別別名在型別解析上沒有多型的意義,對於使用者來說他們非常類似於關聯型別。此語法的目標就是要讓許多使用者能夠輕易使用具有關聯型別建構子的型別,就算他們沒有察覺到這和高階種類有關。

我們如何教導這個

在本 RFC 我們使用「關聯型別建構子」,這是因為這在 Rust 社群中已經很常拿來使用作為此功能的討論了。但這並沒有很輕易地表達此概念,尤其光是型別理論中的「型別建構子」就已經足以讓人望之卻步了,多數使用者可能並不熟悉這樣的詞彙。

在接受本 RFC 後,我們應該開始稱呼此概念為「泛型關聯型別(generic associated types)」就好,現在的關聯型別還無法使用泛型。在此 RFC 後,這就化為可能了。與其教導這是一個不同的功能,不如介紹說這會是關聯型別的進階使用情境。

像是「Family」特徵的模式也需要同時教導,我們可能會寫進書裡或是某種形式的文件就好,像是網誌文章等。

這也可能會增加使用者使用高階特徵界限的頻率,我們可能會需要挹注更多資源在高階特徵界限上。

缺點

增加語言複雜度

這會對語言增加一定的複雜度,型別建構子將能夠多型產生,而且型別系統也需要幾個擴充,這都讓實作更加複雜。

除此之外,雖然語法設計旨在易於學習此功能,但它更有可能產生模糊空間讓使用者意外使用到,而不是他們原本想寫的,這就像 impl .. for Traitimpl<T> .. for T where T: Trait 之間產生的混淆。舉例來說:

#![allow(unused)]
fn main() {
// 使用者其實想寫這樣
trait Foo<'a> {
    type Bar: 'a;
}

// 但他們寫成這樣
trait Foo<'a> {
    type Bar<'a>;
}
}

並非完整的「高階種類型別」

本 RFC 並沒有加入當大家講到高階種類型別時的所有功能。舉例來說,它並沒有辦法提供特徵像是 Monad。有些人傾向於一次實作完所有這些功能。然而,此功能是能夠兼容於其他形式的高階種類多型的,且並沒有將他們的可能性排除掉。事實上,它還解決了一些會影響其他高階種類形式的實作細節,為它們開拓了可能的路徑,像是 Partial Application。

語法與其他形式的高階種類多型都不一樣

雖然建議的語法非常近似於關聯型別與型別別名的語法,其他形式的高階種類多型可能無法使用相同的語法來表達。基於此原因,定義關聯型別建構子的語法可能會與,舉例來說,對特徵實作型別建構子的語法不同。

不過這些其他形式的高階種類多型也將會依到它們實際的功能來決定語法。要設計一個未知功能的語法是非常困難的。

替代方案

進一步強化高階特徵界限而不是關聯型別建構子

其中一種替代方案是強化高階特徵界限,有可能是加入一些省略規則,讓它們更容易使用。

目前可能的 StreamingIterator 也許能被定義成這樣:

#![allow(unused)]
fn main() {
trait StreamingIterator<'a> {
   type Item: 'a;
   fn next(&'a self) -> Option<Self::Item>;
}
}

這樣你就可以綁定型別成 T: for<'a> StreamingIterator<'a> 來避免每次出現 StreamingIterator 時都會被生命週期感染。

然而這樣僅避免了 StreamingIterator 的傳染性,只能用在一些關聯型別建構子能夠表達的型別,而且這更像是針對限制下的權變措施,而非等價的替代方案。

對關聯型別建構子施加限制

我們常稱呼的「完整高階種類多型」能允許使用型別建構子作為其他型別建構子的輸入參數,換句話說就是高階型別建構子。在沒有任何限制的情況下,多重參數的高階型別建構子會對型別介面帶來嚴重的問題。

舉例來說,當你嘗試推導型別時,而且你知道你有個建構子的形式為 type, type -> Result<(), io::Error>,如果沒有任何限制的話,我們很難判斷此建構子到底是 (), io::Error -> Result<(), io::Error> 還是 io::Error, () -> Result<(), io::Error>

有鑑於此,將高階種類多型視為第一公民的語言通常都會對這些高階種類施加限制,像是 Haskell 的柯里(currying)規則。

如果 Rust 也要採納高階型別建構子的話,我們需要對型別建構子可以接收的種類施加類似的限制。但關聯型別建構子已經是一種別名,其自然就囊括了實際型別建構子的結構。換句話說,如果我們想要使用關聯型別建構子作為高階型別建構子的引數,我們需要將那些限制施加於所有的關聯結構建構子。

我們已經有一份我們認為必要且足夠的限制清單,更多細節可以在 nmatsakis 的網誌文章找到:

  • 關聯型別建構子的每個引數都必須被套用到
  • 它們必須以它們在關聯型別建構子出現的相同順序來套用
  • 它們必須套用恰好一次而已
  • 它們必須是建構子的最左引數

這些限制已經非常有建設性了,我們已經知道有一些關聯型別建構子的應用會被受限於此,像是 IterableHashMap 的定義(項目 (&'a K, &'a V) 要套用兩次生命週期)。

有鑑於此,我們決定不要對所有關聯型別建構子施加這些限制。這代表如果到時候有高階型別建構子加入語言中的話,它們將無法將關聯型別建構子作為引數。然而,這其實能透過新型別(newtypes)來滿足限制,舉例來說:

#![allow(unused)]
fn main() {
struct IterItem<'a, I: Iterable>(I::Item<'a>);
}

未解決問題

總結

允許所有型別(type)以作為常數值(constant values)的形式成為泛型(generic),這能讓使用者用 impl 寫出所有陣列型別(array types)。

動機

現在的 Rust 有一種將常數參數化(parametric)型別︰內建的陣列型別 [T; LEN]。然而,常數泛型(const generics)並非第一級別的功能,使用者無法自己定義將常數值作爲泛型的型別,且不能對所有的陣列實作特徵(traits)。

在此限制下,標準庫僅包含長度最多到 32 的陣列實作,而陣列也因此被視爲語言的第二級別的功能。即使在長度可靜態已知的情形下,也大多調配於堆(heap)上成為向量(vector)而非使用陣列型別,因而造成一定程度上的效能取捨。

常數參數也能讓使用者更自然地指定一個泛型變體(variant), 且相比於型別更能確切地反應其值。舉例來說,基於某些因素有個型別用一個名稱來作為參數的話,使用 &'static str 來提供其名稱會比(藉由相關的常數或是函式提供的)單元型別(unit type)的形式還來的合理,這能簡化 API。

總結來說,常數可作爲參數使用,讓編譯器在型別檢查時期能確定這些數值。藉由限制哪些數值是否有實作過特定的特徵,孤兒規則(orphan rules)可以確保一個 crate 僅使用部份安全的數值。(例如有關密碼學的相關函式庫)

設計細節

現今 Rust 中的型別有兩種參數化的形式︰型別與生命周期。如同在編譯時能推導出這些數值,我們將額外的允許型別將數值參數化。一個常數參數必為單一、特定型別,且可以有效的替換為編譯時期計算出的任意值且該型別將符合本 RFC 以下列出的要求。

(為明確在此 RFC 闡明哪些表達式可在編譯時評估,在之後的範例中,我們假設整數及其基本算數操作可以在編譯時計算。)

詞彙表

  • 常數(constant, const value)︰一個在編譯時期可以保證完全評估的 Rust 數值。不同於靜態數值(static),常數將在其使用的位置 inline 而非存在於編譯好的二進制檔案的資料區段。

  • 常數參數、泛型常數(const parameter, generic const)︰一個由型別或函式抽象而得到的常數。此常數是具體型別的輸入值,例如一個靜態陣列的長度。

  • 關聯常數(associated const)︰一個由特徵關聯而得到的常數,其相似於關聯型別。不同於常數參數,關聯常數是由型別所決定的。

  • 常數變數(const variable)︰相比於具體常數,常數參數或關聯常數皆為常數變數。在單態化(monomorphization)之前,一個常數在上下文中是未確定的。

  • 具體常數(concrete const)︰相比於常數變數,一個在上下文中已知且單一值的常數。

  • 常數表達式(const expression)︰一個用於評估一個常數的表達式。此可為一個身份表達式或一個在 Rust 常數系統中可以評估出的更複雜的表達式。

  • 抽象常數表達式(abstract const expression)︰一個包含常數變數的表達式。(因此在單態化結束前其值是無法評估的)

  • 常數投影(const projection)︰抽象常數表達式的數值(其在泛型上下文中因缺乏所依賴的常數變數而無法被定義)

  • 身份表達式(identity expression)︰一個在不以其範圍內的名稱進行置換則無法評估的表達式。此包括了所有的文字及身份(ident),例如,3"Hello, world"foo_bar

宣告常數參數

在型別參數宣告的任何序列中都可以宣告常數參數(例如在一個型別的定義中或是在 impl 的標頭或區塊(block)中)。常數參數的格式爲 const $ident: $ty

#![allow(unused)]
fn main() {
struct RectangularArray<T, const WIDTH: usize, const HEIGHT: usize> {
    array: [[T; WIDTH]; HEIGHT],
}
}

這些宣告的身份(ident)就是常數參數(在本 RFC 內文中可替稱作「常數變數」)的名稱,而且所有數值必定其推導的型別。本 RFC 後文將說明能被推導的型別有哪些限制。

宣告的常數變數範圍在該項目(type、impl、function、method、…等等)的整體範圍中。

套用常數作為參數

任何可推導出該常數參數型別的常數表達式都可作爲參數。除了陣列外,當套用一個表達式作為常數參數且該表達式並非身份表達式時,該表達式必須包含在區塊內 。此語法上的限制是必要的,以避免在型別中解析表達式需要無限地向前展望(lookahead)。

#![allow(unused)]
fn main() {
const X: usize = 7;

let x: RectangularArray<i32, 2, 4>;
let y: RectangularArray<i32, X, {2 * 2}>;
}

陣列

陣列語法中有一個特別的構造語法:[T; CONST]。在陣列中大括號不需要存在於任何的常數表達式中,[i32; N * 2] 是一個合理的型別。

何時可使用常數變數

常數變數可使用於下列上下文之任一項中︰

  1. 用於任一型別中的常數,該型別是待定項目之簽章(signature)的一部份:fn foo<const N: usize>(arr: [i32; N])
  2. 用於定義相關常數的常數表達式或相關型別之參數的一部份
  3. 在項目中任何函式的內部之任何運行時表達式中的數值
  4. 在項目中任何函式內部用於任何型別的參數,例如在 let x: [i32; N]<[i32; N] as Foo>::bar()
  5. 在項目中任何欄位之型別的一部分(如 struct Foo<const N: usize>([i32; N]);

在一般情況下,常數變數可以用做常數。但有一個明顯的例外是,常數變數不可用於常數、靜態型別、函式、或函式內的型別的建構子,意即下面的例子是不合規的。

#![allow(unused)]
fn main() {
fn foo<const X: usize>() {
    const Y: usize = X * 2;
    static Z: (usize, usize)= (X, X);

    struct Foo([i32; X]);
}
}

這樣的限制就如同型別型別不能用在函式本體中建造的型別一樣。所有的宣告雖然專用於該項目,但也必需獨立於它,且不能有任何在其範圍內的參數。

兩個常數的型別相等性之相等原則

在統一且重疊的檢查期間,何時兩個型別是否相等是關重要的。因為型別現在可依賴於常數了,所以我們必須定義我們如何比較兩個常數表達式的相等性。

在大多數情況下,兩個常數的相等性會如同你的預期,如果兩個常數彼此相等,則它們相等。但是仍會有一些特殊的狀況。

結構相等性

常數相等性的定義是根據 RFC 1445 解釋的結構相等性所定義。只有在型別有「結構相符」的特性下,該結構才可用做常數參數。舉例來說,浮點數就會被排除在外。

在最終解決方案出來前,結構相符這樣的性質應作為權宜之計。無論變數參數採用什麼解決方案,確保相等是具有反射性(reflexive)對於型別相等而言是重要的,這樣才能使得型別始終相同。(浮點數相等的標准定義是不具反射性)

這可能會與未來進行配對(match)的定義而有所不同,但是配對和常數參數不一定得使用相同的相等性定義,現今所使用的相等性定義已經足以滿足我們的目的。

因為常數必須具有結構相符的性質,並且此性質無法強制轉成一個型別變數,因此我們無法定義一個常數參數是從其他型別變數推導來的。(意即 Foo <T,const N:T>不是合規的)

兩個抽像表達式的相等性

在比較兩個抽象常數表達式(即依賴於變數的表達式)的相等性時,我們無法比較其值的相等性,因其值是由常數變數所決定的,而在在單態化之前是未知的。

基於這個原因,我們至少會將常數表達式的返回值當作投影(projections)。雖然其值仍保持未知,我們仍藉由輸入值來決定其值,這是作法相同於現在我們處理關聯型別(associated types)的作法。此作法我們將稱為常數投影(const projection),對另一個同型別的常數而言,我們永遠無法確定其相等性。

每個常數表達式都會生成一個新的投影,該投影本質上是匿名的。不可能使兩個匿名投影一致(想像兩個關聯型別的泛型 T :: AssocT :: Item,你無法證明或否定它們是否為同一型別)。因此,除非它們從字面上使用完完全全相同的文字,否則常數表達式在 AST 節點中是彼此不一樣的。這意味著 N + 1 的一個實例不會與另一個 N + 1 的實例在型別相同。

更清楚來說,以下是無法通過型別檢查的,因為 N + 1 會是兩種不同的型別:

#![allow(unused)]
fn main() {
fn foo<const N: usize>() -> [i32; N + 1] {
    let x: [i32; N + 1] = [0; N + 1];
    x
}
}

但如果這樣寫的話,它將只有一個型別:

#![allow(unused)]
fn main() {
type Foo<const N: usize> = [i32; N + 1];

fn foo<const N: usize>() -> Foo<N> {
    let x: Foo<N> = Default::default();
    x
}
}

未來的擴展性

未來的某一天我們可以利用一些操作的基本性質(例如加法和乘法的可交換性),而對常數投影的相等性做出更聰明的判斷。但是,本 RFC 並不打算在此提案中建造這些可能性,而是打算留給未來的 RFC 決定。

常數變數的特化(Specialization)

定義常數參數特化的順序也是很重要的。為此,將字面上的定義必需比其他表達式更具體,否則表達式定義的順序上也會產生不確定性。

正如我們有朝一日可以在常數投影上支援更進階的相等性一樣,我們可以支援更進階的特化定義。例如,給定型別為 (i32, i32),我們可以確定 (0, PARAM2)(PARAM1, PARAM2) 更為具體;(i32, U)(T, U) 更為具體。在未來的某天我們將也有可能在常數特化上支援多元交互(intersectional)和其他更進階的定義。

我們如何教導這個

常數泛型是一個很龐大的功能,將需要大量的教育資源,這將會需要寫在在書本跟參考文件中,且可能會在書中有獨立的章節。常數泛型的文件化過程本質上將會是一個大工程。

然而,常數泛型應該被視為進階功能,並且在使用 Rust 的初期,我們應該不會向新手介紹這些內容。

缺點

此功能由於允許型別由常數決定,將為型別系統增加了大量的複雜性。它需要抽象變數相等性的確定規則,這出現了很多令人意外的的特殊情況。它增加了 Rust 的很多語法。如果我們不要採用此功能,Rust 肯定會更簡單。

然而,我們已經引入了一種由常數確定的型別(數組型別)。泛型化的功能似乎是必然的,甚至是不可避免的,鑑於此,我們應該盡早決定。

替代方案

除了暫緩執行或不執行外,並沒有真的替代方案。

我們可以限制常數泛型為 usize 型別,但這不會讓實作更為簡單。

我們可以對常數相對性的複雜概念更積極地發展,但這會使實作比上述說明得更加複雜。

我們可以選擇稍微不同的語法,例如將在常數跟型別間加上分號。

未解決問題

  • 一致的抽象常數表達式︰本 RFC 盡可能最大限度地減少抽象常數表達式的統合性上的處理,從本質上來說,並沒有使其一致。這可能造成無法接受的使用者體驗的不穩定,而我們想要實作一些更進些的統合性之前穩定此功能。
  • 常數表達式之正確格式︰只有在單態化過程中,程式不會恐慌(panic)的情況下,此型別才能視爲正確格式。這對於溢出和超出範圍的陣列存取來說很棘手。然而,我們實際上只能確保在函式的簽章中表達式常數的格式進行正確性約束。目前尚不清楚有關在函式中抽象常數表達式之格式正確性的處理方式,也因此使實作推遲。
  • 排序與預設參數︰所有常數參數是否將排在最後,或者將它們與型別混合嗎?具有預設值的參數是否須在沒有預設值之後?這些決定推遲到實作語法的討論中。

總結

透過此功能,可以在常數求值(const evaluation)中使用 ifmatch 並使他們被延遲求值。簡單來說,這項功能允許我們寫 if x < y { y - x } else { x - y };即使當使用非負型別時在 x < yelse 分支會報溢位錯誤 (overflow error)。

動機

在常數宣告中使用條件式對於建立像是 NonZero::newconst fn 及 直譯判定(interpreting assertions)來說很重要。

教學式解說

如果你寫

#![allow(unused)]
fn main() {
let x: u32 = ...;
let y: u32 = ...;
let a = x - y;
let b = y - x;
if x > y {
    // do something with a
} else {
    // do something with b
}
}

這支程式永遠都會 panic(除非 xy 同時是 0)因為不管是 x - y 或是 y - x 都會造成溢位。為了解決此問題,我們必須把 let alet b 個別搬進 ifelse 中。

#![allow(unused)]
fn main() {
let x: u32 = ...;
let y: u32 = ...;
if x > y {
    let a = x - y;
    // do something with a
} else {
    let b = y - x;
    // do something with b
}
}

當改用常數時,上面的寫法就會出現新問題:

#![allow(unused)]
fn main() {
const X: u32 = ...;
const Y: u32 = ...;
const FOO: SomeType = if X > Y {
    const A: u32 = X - Y;
    ...
} else {
    const B: u32 = Y - X;
    ...
};
}

AB 會比 FOO 先被求值,因為常數在定義上就是「常數」,所以不應被求值順序影響。這項假設在有錯誤的情況下並不成立,因為錯誤屬於副作用(side effects),因此不純(pure)。

為了解決此問題,我們必須把中介常數消掉並改為直接對 X - YY - X 求值。

#![allow(unused)]
fn main() {
const X: u32 = ...;
const Y: u32 = ...;
const FOO: SomeType = if X > Y {
    let a = X - Y;
    ...
} else {
    let b = Y - X;
    ...
};
}

技術文件式解說

if 或是在 variant 沒有欄位的 enums 上做 match 會在 HIR -> MIR 階段時,被轉譯成 switchInt 終止器(terminator)。Mir 直譯器現在將會針對那些終止器求值(之前就可以了)。

在 variant 沒有欄位的 enums 上做 match 會被轉譯成 switch,表示他會被檢查 discriminant 或是在 packed enums(例如 Option<&T>)的情況會運算 discriminant(這個情況 discriminant 沒有特別的記憶體位址,但他會把所有的零視為 None,並把其他的值都當作 Some)。當進入 match 的分支時,匹配上的值基本上會被 transmute 成 enum 的 variant 型別,如此一來可以允許其他程式碼來存取該 enum 的欄位。

缺點

這項功能容易造成任意「常數」值(如:size_of::<T>() 或是特定的平台常數)編譯失敗。

原理及替代方案

利用中介 const fns 來破壞立即常數求值(eager const evaluation)

如果寫成

#![allow(unused)]
fn main() {
const X: u32 = ...;
const Y: u32 = ...;
const AB: u32 = if X > Y {
    X - Y
} else {
    Y - X
};
}

X - Y 或是 Y - X 其中一方有可能會報錯,這時必須加入中介 const fn

#![allow(unused)]
fn main() {
const X: u32 = ...;
const Y: u32 = ...;
const fn foo(x: u32, y: u32) -> u32 {
    if x > y {
        x - y
    } else {
        y - x
    }
}
const AB: u32 = foo(X, Y);
}

const fn 的 xy 參數未知,無法做常數求值(const evaluate)。當提供此 const fn 參數並求值時,只會對相應的分支求值。

未解決問題

概要

新增 async 和 await 語法,使撰寫程式碼操作 futures 更符合人因工程學。

另有一個配套 RFC,用於向 libstd 和 libcore 新增一個小型 futures API。

動機

高效能網路服務經常使用非同步 IO,而不是阻塞式 IO,這樣在處理許多並行連線時更容易獲得更好的效能表現。 Rust 已經在網路服務領域得到了一些採用,我們希望透過使 Rust 中撰寫非同步網路服務更符合人因工程學,繼續支援這些使用者 - 並支援其他使用者採用。

Rust 中非同步 IO 的發展經歷了多個階段。在 1.0 之前,我們嘗試在語言中內嵌綠色執行緒(green-threading) runtime。然而,事實證明這太過武斷了——因為它影響了每個用 Rust 撰寫的程式——並且在 1.0 之前不久就被刪除了。在 1.0 之後,非同步 IO 最初的重點是 mio 函式庫,它為 Linux、Mac OS 和 Windows 的非同步 IO 基元(primitive)提供了一個跨平台抽象。 2016 年年中,future crate 的引入產生了重大影響,它為非同步操作提供了一個方便且可共享的抽象。 tokio 函式庫提供了一個基於 mio 的事件循環,可以執行使用 future 介面實作的程式碼。

在獲得基於 future 的生態系統的經驗和使用者的回饋後,我們發現了某些人因工程學挑戰。使用需要在等待點(await point)之間共享的狀態,是非常不符合人因工程學的(需要 Arc 或 join chaining)雖然組合器通常比手動撰寫 future 更符合人因工程學,但它們仍然經常導致混亂的嵌套和 chained callbacks。

幸運的是,Future 抽象非常適合與一種語法糖一起使用——這個語法糖在許多具有非同步 IO 的語言中愈來愈常見:async 和 await 關鍵字。簡而言之,非同步函式回傳一個 future,而不是在呼叫時立即執行。在函式內部,可以使用 await 表達式等待其他 future,這使它們在輪詢 future 時讓出控制權。從使用者的角度來看,他們可以像在使用同步程式碼一樣使用 async/await,並且只需要標注其函式和調用。

Async/await 和 futures 通常是非同步和並行的強大抽象,並且可能可以應用在非同步 I/O 空間以外的地方。我們今天遇到的案例通常與非同步 IO 相關,但透過引入一等公民語法和 libstd 的支援,我們相信更多不直接與非同步 IO 相關的 async 和 await 案例也會蓬勃發展。

教學式解說

非同步函式

對函式加上 async 關鍵字,使它們成為「非同步函式」:

#![allow(unused)]
fn main() {
async fn function(argument: &str) -> usize {
     // ...
}
}

非同步函式的工作方式與普通函式不同。呼叫非同步函式時,它不會立即進入主體。相反,它執行實作 Future 特徵的匿名型別。當輪詢該 future 時,該函式被執行到它內部的下一個 await 或回傳點(請參閱接下來 await 語法部分)。

非同步函式是延遲計算的一種 - 在您開始輪詢函式回傳的 future 之前,函式本體中沒有任何內容被實際執行。例如:

async fn print_async() {
     println!("Hello from print_async")
}

fn main() {
     let future = print_async();
     println!("Hello from main");
     futures::executor::block_on(future);
}

這將在印出 "Hello from main" 之前印出 "Hello from print_async"

async fn foo(args..) -> Tfn(args..) -> impl Future<Output = T> 型別的函式。回傳型別是編譯器產生的匿名型別。

async || closures

除了函式,非同步也可以應用在 closure 上面。與非同步函式一樣,非同步 closure 的回傳型別為 impl Future<Output = T>,而不是 T。當您呼叫該 closure 時,它會立即回傳一個 future,且不會執行任何程式碼(就像非同步函式一樣)。

fn main() {
    let closure = async || {
         println!("Hello from async closure.");
    };
    println!("Hello from main");
    let future = closure();
    println!("Hello from main again");
    futures::block_on(future);
}

這將在印出 "Hello from async closure." 之前印出兩個 "Hello from main"

async closure 可以用 move 來捕捉它們包覆在 closure 內的變數的所有權。

async 區塊

您可以使用 async 區塊直接將 future 建立為表達式:

#![allow(unused)]
fn main() {
let my_future = async {
    println!("Hello from an async block");
};
}

這種形式幾乎等同於立即呼叫的 async closure。即是:

#![allow(unused)]
fn main() {
async { /* body */ }

// is equivalent to

(async || { /* body */ })()
}

除了像 returnbreakcontinue 這樣的控制流程結構不允許在 body 中使用(除非它們出現在一個新的控制流上下文中,比如 closure 或 loop)。 尚未確定 ? 運算子和提早回傳(early return)在非同步區塊運作的方式(請參閱未解決的問題)。

async closure 一樣,async 區塊可以加入 move,以捕捉區塊內所包覆的變數的所有權。

編譯器內嵌的 await!

編譯器加入了一個名為 await! 的內建函式。await! 可用於「暫停」future 的計算,將控制權交還給呼叫者。await! 接受任何實作 IntoFuture 的表達式,並計算為此 future 所傳入的泛型型別(如下面範例的 Output)之值。

#![allow(unused)]
fn main() {
// future: impl Future<Output = usize>
let n = await!(future);
}

await 展開的程式碼,會重複在接收到的 future 上呼叫 pollpoll 回傳 Poll::Pending 時讓出 (yield) 函式的控制權,並在最終回傳 Poll::Ready 時取得項目的值。

await! 只能在非同步函式、closure 或區塊內使用,除此之外使用它都是錯誤的。

await! 是編譯器的內建函式,為以後確定其確切語法保留彈性空間。詳細資訊請參閱〈未解決的問題〉部分。)

技術文件式解說

關鍵字

asyncawait 在 2018 版本中都成為關鍵字。

async 函式、closure、區塊的回傳型別

非同步函式的回傳型別是編譯器生成的唯一匿名型別,和 closure 的型別類似。你可以把這種型別想像成一個枚舉,函式的每個「yield point」都是一個變體——開頭、await 表達式和每一次的回傳。每個變體都會儲存需要保存的狀態,以便從該 yield point 恢復控制。

呼叫函式時,此匿名型別以其初始狀態回傳,其中包含此函式的所有引數。

特徵綁定

匿名回傳型別實作 FutureItem 為它的回傳型別。輪詢它會推進函數的狀態,當它在 await 狀態時會返回 Pending;當它在 return 狀態時則返回 Ready。任何在它已經回傳 Ready 一次後對其嘗試進行輪詢都將造成恐慌。

匿名回傳型別對 Unpin 特徵有一個相反的實作,即 impl !Unpin。這是因為 future 可能有內部引用,這意味著它永遠不需要被移動。

匿名 future 的生命週期捕捉

該函式的所有輸入生命週期都在非同步函式回傳的 future 捕捉,因為它將函式的所有引數儲存在其初始狀態(可能還有以後的狀態)。也就是說,給定這樣的函數:

#![allow(unused)]
fn main() {
async fn foo(arg: &str) -> usize { ... }
}

它具有與此等效的類型簽名:

#![allow(unused)]
fn main() {
fn foo<'a>(arg: &'a str) -> impl Future<Output = usize> + 'a { ... }
}

這與 impl Trait 的預設值不同,它不捕捉生命週期。這就是為什麼回傳類型是 T 而不是 impl Future<Output = T> 的一個重要部分。

「初始化」模式

有時會出現的一種模式是 future 有一個「初始化」步驟,應該在其建立期間被執行。這在處理資料轉換和臨時借用時很有用。因為 async 函式在您輪詢它之前不會開始計算,並且它會捕捉其引數的生命週期,因此這種模式不能直接用 async fn 表示。

其中一個解決辦法,是撰寫一個回傳 impl Future 的函式,而回傳值是會立即計算 (evaluate) 的 closure:

#![allow(unused)]
fn main() {
// only arg1's lifetime is captured in the returned future
fn foo<'a>(arg1: &'a str, arg2: &str) -> impl Future<Output = usize> + 'a {
    // do some initialization using arg2

    // closure which is evaluated immediately
    async move {
         // asynchronous portion of the function
    }
}
}

await 展開後的程式碼

內嵌的 await! 展開結果大致如下:

#![allow(unused)]
fn main() {
let mut future = IntoFuture::into_future($expression);
let mut pin = unsafe { Pin::new_unchecked(&mut future) };
loop {
    match Future::poll(Pin::borrow(&mut pin), &mut ctx) {
          Poll::Ready(item) => break item,
          Poll::Pending     => yield,
    }
}
}

這不是真正意義上的『展開』,因為 yield 概念不能用 async 函式中的表層語法來表達。這就是為什麼 await! 是一個內建編譯器函式,而不是實際的巨集。

asyncmove 的順序

非同步 closure 和區塊可以用 move 註釋來捕捉它們包覆的變數的所有權。關鍵字的順序固定為 async move。只允許一種順序,可以避免語義上「是否有重要意義」的混淆。

#![allow(unused)]
fn main() {
async move {
    // body
}
}

缺點

在 Rust 中新增 async 和 await 語法是對語言的重大更改 - 這是自 1.0 以來最重要的新增功能之一。雖然我們從最小的功能開始,但從長遠來看,它所隱含的功能集也會增長(請參閱未解決的問題部分)。對於這樣一個重要的新增功能,我們絕不能掉以輕心,只有在強烈的動機下才能進行。

我們相信,一個符合人因工程學的非同步 IO 解決方案對於 Rust 作為撰寫高效能網路服務的語言的成功至關重要,這是我們 2018 年的目標之一。基於 Future trait 的 async & await 語法是在不久的將來實現這一目標最便捷和低風險的途徑。

這個 RFC,連同其配套的 lib RFC,對 future 和 async/await 做出比我們過往的專案更堅定的承諾。如果我們在穩定這些特性之後決定反其道而行,那將付出相當大的代價。因為這個 RFC 的存在,增加非同步程式的替代機制的成本會更高。然而,有鑑於我們在 future 方面的經驗,我們相信這是正確的發展方向。

我們所做的幾個小決定也有缺點。例如,在使用「内部」回傳型別和「外部」回傳型別之間有一個權衡。我們可以為非同步函式建立一個不同的求值模型,即在第一個等待(await point)點之前立即對其進行求值。我們在這些問題上做出的决定在 RFC 的相應部分都有說明。

原理和替代方案

本節包含了本 RFC 拒絕的替代性設計決定(相對於那些只是推遲的設計)。

回傳型別(使用 T 而不是 impl Future<Output = T>

非同步函式的回傳型別是一個有點複雜的問題。對於非同步函式的回傳型別,有兩個不同的觀點:「内部」回傳型別 - 你用 return 關鍵字回傳的型別,以及「外部」回傳型別 - 當你呼叫函式時回傳的型別。

大多數帶有非同步函式的靜態型別語言在函式簽名中顯示「外部」回傳型別。本 RFC 建議在函式簽名中顯示「内部」回傳型別。這既有優點也有缺點。

生命週期消除問題

正如前面所提到的,回傳的 future 捕捉了所有傳入的生命週期。預設情況下,impl Trait 不捕捉任何生命週期。為了準確反應外部回傳型別,有必要消除生命週期的省略:

#![allow(unused)]
fn main() {
async fn foo<'ret, 'a: 'ret, 'b: 'ret>(x: &'a i32, y: &'b i32) -> impl Future<Output = i32> + 'ret {
     *x + *y
}
}

這將是非常不符合人因工程學的,並且使非同步的使用變得更不愉快,更不易於學習。這個問題在決定回傳內部型別時佔很大比重。

我們可以讓它回傳 impl Future,但對於 async fn 的回傳型別,生命週期捕捉的運作方式與其他函式不同,這似乎比顯示內部型別更糟糕。

多型的回傳(對我們來說並非是一個因素)

根據 C# 開發者的說法,回傳 Task<T>(他們的「外部型別」)的主要因素之一是,他們希望有可以回傳 Task 以外型別的非同步函式。我們對此沒有一個可以令人信服的實例。

  1. 在 future 的 0.2 分支中,FutureStableFuture 之間是有區別的。然而,這種區分是人為的,單純是因為物件安全(object-safe)的自定義自型別(self-types)在穩定版本上還不能使用。
  2. 目前的 #[async] 巨集有一個 (boxed) 變體。我們更傾向於讓非同步函式盡可能的不包裝,只在呼叫處明確包裝。屬性變體的動機是為了支援物件安全特徵中的非同步方法。這是在物件安全特徵中支援 impl Trait 的一個特例(可能是透過在物件情况下對回傳型別進行包裝),我們希望這個特性與非同步函式分開。
  3. 有人建議我們支援回傳串流(stream)的 async fn。然而,這意味著内部函式的語意在回傳 future 和串流的函式之間會有顯著的不同。正如在未解决的問題部分所討論的,基於生成器和非同步生成器的解決方案似乎更有機會。

基於這些原因,我們認為從多型的角度來看,回傳外部型別的論點並不強烈。

可學習性/文件的權衡

從可學習性的角度來看,支援外部和内部回傳型別的論點都有。支援外部回傳型別最有說服力的論據之一是文件:當你閱讀自動產生的 API 文件時,你肯定會看到你作為呼叫者得到的東西。相較之下,由於回傳型別和你 return 的表達式的型別之間的對應關係,可以更容易理解如何使用内部回傳型別撰寫非同步函式。

Rustdoc 可以透過幾種方式處理使用内部回傳型別的非同步函式,使其更容易理解。我們至少應該確保在文件中包含 async 註解,這樣了解 async 符號的使用者就知道此函式將回傳一個 future。我們還可以進行其他轉換,可能是可選的,以顯示函式的外部簽名。如何確切處理非同步函式的 API 文件是一個尚未解决的問題。

內嵌語法,而不是在生成器中使用巨集

另一個選擇是專注於穩定程序性巨集(procedural macro)和生成器,而不是為非同步函式引入內嵌語法。一個非同步函式可以被建模為一個生成器,它將產生 ()

從長遠來看,我們相信我們會希望有專門的語法來處理非同步函式,因為它更符合人因工程學原理,而且使用情境也足夠令人信服和重要,可以證明這一點(類似於 - 例如 - 有内嵌的 for 迴圈和 if 判斷式,而不是有編譯成迴圈和配對判斷式的巨集)。鑑於此,唯一的問題是,我們是否可以透過暫時使用生成器來獲得比現在引入非同步函式更好的穩定性。

使用展開到生成器的巨集似乎不太可能使其更快的穩定。生成器可以表現更多的可能性,並且有更多的開放性問題 - 包括語法和語意。這甚至沒有解決穩定更多程序性巨集的開放問題。出於這個原因,我們認為穩定最小的内嵌 async/await 功能比試圖穩定生成器和 proc 巨集更有效益。

單純基於生成器的 async

另一種設計是將非同步函式作為建立生成器的語法。在這種設計中,我們可以寫一個這樣的生成器:

#![allow(unused)]
fn main() {
async fn foo(arg: Arg) -> Return yield Yield
}

return 和 yield 都是可選的,預設為 ()。一個產生 () 的非同步函式將使用全面實作(blanket impl)來實作 Future。一個回傳 () 的非同步函式將實作 Iterator

此方法的問題是,它不能從人因工程學的角度處理 Stream,Stream 需要產生 Poll<Option<T>>。目前還不清楚在一個產生 () 以外的東西(包括 Stream)的非同步函式裡的 await 如何運作。由於這個原因,「矩陣」方法,即我們對生成器函式、非同步函式和非同步生成器函式有獨立的語法,似乎是一個更可行的方法。

"Hot async functions"

正如本 RFC 所建議的,所有的非同步函式都會立即回傳,根本不需要執行其主體。如上所述,這對於需要立即進行「初始化」步驟的情境來說並不方便 - 例如,這些情境需要使用一個終端非同步區塊。

另一種方法是讓非同步函式立即求值,直到它們的第一個 await,在那之前保留它們的狀態。這將是一個相當複雜的實現 - 它們需要在 await 中擁有一个額外的 yield point,在輪詢被 await 的 future 之前,條件是 await 是否是 future 主體中的第一個 await。

Rust 的 future 與其它語言的 future 的一個根本區別是,Rust 的 future 除非被輪詢,否則不會做任何事情。整個系统都是圍繞這一點建立的:例如,取消正是因為這個原因而捨棄了 future。相反,在其它語言中,呼叫一個非同步函式會產生一個立即開始執行的 future。這種差異也延續到了 async fnasync 區塊中,其中至關重要的是,產生的 future 要主動輪詢以取得進展。允許部分、急迫的執行很可能會引發嚴重的混亂和錯誤。

從使用者的角度來看,這也很複雜 - 主體的一部分何時被執行取決於它是否出現在所有 await 語句(可能是巨集生成的)之前。使用终端 async 區塊提供了一個更清晰的機制來區分帶有初始化步驟的 future 中立即執行部分和非同步執行部分。

使用 async/await 而不是其他的非同步性系統

最後,一個極端的選擇是放棄 future 和 async/await 作為 Rust 中 async/await 的機制,而採用不同的典範。在這些建議中,有一個常見的效果系统,monad 和 do 語法(do notation)、綠色執行緒和滿堆疊(stack-full)的協程。

假設性上來說,Rust 可以透過 async/await 語法來達成一些泛化(generalization),但在這個領域的研究還不足以在短期内支援它。考慮到我們 2018 年的目標 - 強調 - async/await 語法(一個在許多語言中廣泛存在的概念,與我們現有的 async IO 函式庫運作良好)是在 Rust 發展的這個階段最合理的實作。

非同步區塊與非同步 closure

正如文中所指出的,非同步區塊和非同步 closure 是密切相關的,而且大致上是可以相互表達的:

#![allow(unused)]
fn main() {
// almost equivalent
async { ... }
(async || { ... })()

// almost equivalent
async |..| { ... }
|..| async { ... }
}

我們可以考慮只採用兩個結構中的其中一個。然而:

  • 為了與 async fn 保持一致,我們有充分的理由使用 async ||;這樣的 closure 通常對建構一個服務這樣的高階構造很有用。

  • 有一個強而有力的理由讓我們採用非同步區塊。RFC 文件中提到的初始化模式,以及事實上它提供了一種更直接、更原始的方式建立 future。

RFC 提議在前面就包含這兩個構造,因為我們似乎不可避免地需要這兩者,但我們總是可以在穩定之前重新思考這個問題。

現有技術

在其他語言中,包含 C#、JavaScript 和 Python,有很多關於使用 async/await 語法作為處理非同步操作的一種方式的先例。

目前主流的非同步程式設計有以下三種範式:

  • async 和 await 符號。
  • 隱式並行的執行階段程式庫(implicit concurrent runtime),通常稱為「綠色執行緒」,例如通信順序行程(例如 Go)或參與者模型(例如 Erlang)。
  • 延遲執行程式中的 Monadic 轉換,例如:Haskell 的 do 語法(do notation)。

async/await 是 Rust 最引人注目的模型,因為它與所有權和借用互動良好(不像基於 Monadic 的系统),而且它使我們能夠擁有一個完全基於函式庫的非同步模型(不像綠色執行緒)。

我們對 async/await 的處理不同於大多數其他靜態型別語言(例如 C#),我們選擇顯示「內部」回傳型別,而不是外部回傳型別。正如在替代方案部分中討論的那樣,在 Rust 定義的特定脈絡下(生命週期省略,不需要回傳型別多型),這種偏差的動機充分。

未解決的問題

本節包含已推延且未包含在此初始 RFC 中設計的延伸。

await 表達式的最終語法

儘管此 RFC 建議 await 是一個內建的巨集,但我們希望有一天它成為一個正常的控制流結構。如何處理它的運算子優先順序,以及是否需要某種分隔符號則尚待解決。

特別是, await? 有一個有趣的互動。很常見的情況是有一個 future,它將被執行為一個 Result,然後使用者會想把這個结果應用到 ? 這意味著 await 應該比 ? 有更高的優先順序,這樣該模式就能按照使用者的意願運作。然而,由於它引入了一個空格,看起來這並不是你要得到的優先順序:

await future?

以下有幾種可能的解決方案:

  1. 需要某種的分隔符號,可能是大括號或括號或兩者之一,讓它看起來更符合期望的那樣 - await {future}? - 這很煩躁。
  2. 將優先順序定義為,如果優先順序不符使用者本意,需要使用者明確指出 (await future)? - 對使用者來說非常令人驚訝。
  3. 將其定義為不方便的優先順序 —— 這似乎與其他優先順序一樣令人驚訝。
  4. 引入一種特殊的語法來處理多個應用程式,例如 await? future - 這似乎是很不尋常的方式。

這個問題留給未來找尋另一種解決方案,或是從上述方案中選擇最不糟糕的一個。

for await 和處理串流

RFC 目前遺漏的另一個延伸是使用 for 迴圈處理串流的能力。可以想像 for await 這樣的結構,它採用 IntoStream 而不是 IntoIterator

#![allow(unused)]
fn main() {
for await value in stream {
    println!("{}", value);
}
}

這被排除在最初的 RFC 之外,以避免必須在標準庫中穩定 Stream 的定義(以使與此相關的 RFC 盡可能小)。

生成器和串流

將來,我們可能還希望能夠定義對串流求值非同步函式,而不是對 future 求值。我們建議透過生成器來處理這個案例。生成器可以轉換為一種迭代器,而非同步生成器可以轉換為一種串流。

例如(使用的語法可能會改變);

#![allow(unused)]
fn main() {
// Returns an iterator of i32
fn foo(mut x: i32) yield i32 {
     while x > 0 {
          yield x;
          x -= 2;
     }
}

// Returns a stream of i32
async fn foo(io: &AsyncRead) yield i32 {
    async for line in io.lines() {
        yield line.unwrap().parse().unwrap();
    }
}
}

實現 Unpin 的非同步函式

如本 RFC 中所提議,所有非同步函式均未實現 Unpin,因此將它們從 Pin 中移出是不安全的。這允許它們包含跨越 yield point 的引用。

我們還可以透過註釋對非同步函式進行型別檢查,以確認它不包含任何跨越 yield point 的引用,從而允許它實做 Unpin。可啟用此功能的註釋暫時未指定。

異步區塊中的 ? 運算子和控制流構造

這個 RFC 沒有提出 ? 運算子和控制流結構如 returnbreakcontinue 應該如何在非同步區塊中工作。

不過有討論非同步區塊應該充當 ? 運算子的邊界。讓它們適用於易出錯的 IO:

#![allow(unused)]
fn main() {
let reader: AsyncRead = ...;
async {
    let foo = await!(reader.read_to_end())?;
    Ok(foo.parse().unwrap_or(0))
}: impl Future<Output = io::Result<u32>>
}

此外,還討論了允許使用 break 從非同步區塊中提前回傳:

#![allow(unused)]
fn main() {
async {
    if true { break "foo" }
}
}

使用 break 關鍵字而不是 return 可能有助於表明它適用於非同步區塊而不是其周圍的函式。另一方面,這會給使用 return 關鍵字的 closure 和非同步 closure 帶來區別。

總結

Cargo.toml 的 package 區塊加入 rust 欄位,用於指定 crate 的最低支援 Rust 版本(Minimum Supported Rust Version,MSRV)。

[package]
name = "foo"
version = "0.1.0"
rust = "1.30"

動機

當前 crate 無任何正式方法指定 MSRV,導致使用者無法在不建構該 crate 的情況下得知 crate 是否可透過他們的工具鏈(toolchain)建構。這也帶來有關如何在提升 MSRV 時管理 crate 版本的爭論,保守的作法是將之視為破壞性改動,這種作法會阻礙整個生態採納新功能,或使得版本號通膨式提升,進而讓下游的 crate 很難跟上。另一方面,若作法稍微寬鬆些,則導致採用較舊編譯器版本的使用者無法成功編譯它們的 crate。

教學式解說

若你想指定一個特定 MSRV 版本,請在 Cargo.toml[package] 區塊中的 rust 欄位設定指定的 Rust 版本。如果你建構一個 crate 時有任一依賴要求的 MSRV 比你當前工具鏈更高,會導致編譯錯誤,指明該依賴與它的 MSRV。這個行為可以透過 --no-msrv-check 選項來停用。

技術文件式解說

在建構過程(包含 runtestbenchmarkverifypublish 子指令),cargo 會以依賴樹(dependency tree)的形式,檢查所有將要建構或檢查的所有 crate 之 MSRV 要求。在依賴樹內但不會被構建的 crate 不會執行此項檢查(例如特定目標平台或可選的 crate)。

rust 欄位應至少遵循下列要求:

  • 其值需遵守語意化版號且不能有範圍運算子。注意,「1.50」是合法的值,代表「1.50.0」。
  • 版本不能比當前 stable 工具鏈高(上傳 crate 時 crates.io 會檢查)。
  • 版本不能低於 1.27(此版本將 package.rust 欄位從錯誤改為警告)。
  • 版本不能比選用的 edition 釋出的版本低,舉例來說,同時出現 rust = "1.27"edition = 2018 是非法的。

未來展望與延伸

對版本解析之影響

rust 欄位之值(手動設定或 cargo 自動選擇)會用於選擇合適的依賴版本。

舉例來說,想像你的 crate 相依 crate foo,一個發佈了從 0.1.00.1.9 十個版本的 crate,其中 0.1.00.1.5 版在 crates.io 上 Cargo.tomlrust 欄位為「1.30」,其他版本則為「1.40」。現在,你建構一個專案用了例如 Rust 1.33 版,cargo 會選用 foo v0.1.5foo v0.1.9 只會在你用 Rust 1.40 或更高版本建構專案時選用。倘若你嘗試使用 Rust 1.29 建構專案,cargo 會回報錯誤。

rust 欄位值也會被檢核。在 crate 建構過程,cargo 將檢查所有上游相依是否可以在指定 MSRV 下建構。(例如,檢查給定的 crate 與 Rust 版本限制條件下是否存在解)已除去(yank)的 crate 會略過這個檢查步驟。

期望實作這項功能得以替長久以來對 MSRV 提升是否為破壞性改動的爭論劃下休止符,並讓 crate 作者提升 crate 的 MSRV 不再如此綁手綁腳。(儘管對於已 1.0 的 crate 來說,透過提升修訂號(patch version)來提升 MSRV 次版號,以接納修復嚴重問題的 backport,可能是個有用的慣例)

注意,上述 MSRV 限制與依賴版本解析檢查,可以透過 --no-msrv-check 選項停用。

發佈時檢查 MSRV

cargo publish 將檢查上傳是以 rust 欄位指定的工具鏈版本完成,若工具鏈版本有異,cargo 會拒絕上傳該 crate。此確保機制避免因為未預期的 MSRV 提升導致錯誤的 rust 欄位值。這項檢查可透過既有的 --no-verify 選項停用。

rust 欄位設為必填

未來(可能是下一個 edition),我們可以設定新上傳的 crate 的 rust 欄位為必填。既有 crate 的 MSRV 會透過 edition 決定。換句話說 edition = 2018 之 MSRV 必然是 rust = "1.31",而 edition = "2015" 則是 rust = "1.0"

cargo init 將會使用當前工具鏈使用的版本。

基於 cfg 的 MSRV

部分 crate 會根據不同目標架構平台或啟用的功能而有不同的 MSRV。可透過以下方式有效指定 MSRV 如何依賴這些配置:

[package]
rust = "1.30"

[target.x86_64-pc-windows-gnu.package]
rust = "1.35"

[target.'cfg(feature = "foo")'.package]
rust = "1.33"

target 區塊中所有 rust 值應等於或高於在 package 區塊的 rust 值。

target 的條件為真,cargo 會取用該區塊的 rust 值。若多個 target 區塊的條件為真,則取用最大值。

Nightly 與 stable 版本

部分 crate 可能偏好在最新 stable 或 nighly 工具鏈,除了指定版本之外,我們可允許宣告 stablenightly 值,讓維護者不需追蹤該 crate 的 MSRV 。

對於某些超前沿的 crate(例如: rocket)常常因為 Nightly 更新就壞,將可指定特定可成功建構的 Nightly 版本。透過下列語法來達成:

  • 自動選擇:nightly 此寫法與寫 stable 的行為一致,將使用等於當前或更高的 nightly 版本。
  • 單一版本:nightly: 2018-01-01 (主要寫法)
  • 列舉:nightly: 2018-01-01, 2018-01-15
  • 類語意化版本條件:nightly: >=2018-01-01nightly: >=2018-01-01, <=2018-01-15nightly: >=2018-01-01, <=2018-01-15, 2018-01-20。(後者會解讀為 「(version >= 2018-01-01 && version <= 2018-01-20) || version == 2018-01-20」)

這些條件或許很嚴苛,盼使用這功能的 crate 一隻手數得出來。

缺點

  • 即使宣告了 MSRV 且檢查過,並無法保持 crate 能夠正確在指定 MSRV 下正確執行,只有合理配置的 CI 能做到此事。
  • 更複雜的依賴版本解析演算法。
  • 使用 cargo publish 配合 MSRV rust = "stable" 恐過於保守。

替代方案

  • 自動計算 MSRV。
  • 不做任何事,依靠 LTS 發行 的 crate MSRV 提升。
  • 允許在 RFC 2523 中提出基於版本與路徑的 cfg 屬性(attribute)

先驅技術

早先的提案:

未解決問題

  • 瑣碎的命名問題:rustrustc 還是 min-rust-version
  • 額外的檢查?
  • 更優質地說明版本解析演算法
  • nightly 版本如何與「基於 cfg 的 MSRV」運作?

概要

此 RFC 提議替一等公民語法 async/await 穩定相關函式庫組件。特別是,它將穩定:

  • std 中任務系統所有的 API,例如 std::task::*
  • 核心 Future API,例如 core::future::Futurestd::future::Future

不提議穩定任何 async/await 語法本身,該語法自有另外的提議。它也不包括已在別處提出的 Pin API 的穩定性。

這是早期 futures RFC 的修訂和精簡版本,該 RFC 已暫緩,直到 nightly 版本獲得更多經驗之後。

動機

為何 Future 要在 std 之中?

此 RFC 的核心動機是穩定 async/await 語法的支援機制。語法本身是在(已經合併的)配套 RFC 中提出的,並且有一篇部落格文章更詳細介紹它的重要性。

與 closures 一樣,async 語法涉及到生成一個匿名型別,該型別實作一個關鍵特徵:Future。因為 async/await 需要語言層級的支援,所以底層特徵也必須是標準函式庫的一部分。因此,這個 RFC 的目標是穩定這個 Future 特徵和它所依賴的型別。這是我們能夠穩定 async/await 本身之前所需的最後一步。

這一步是如何融入更大的藍圖之中?

async/await 語法是 Rust 中最渴求的特性之一,它將對整個生態系產生重大影響。自 2018 年 5 月下旬以來,該語法及此處描述的 API 已可於 nightly 版本使用,並被大量採用。

穩定此設計的 futures API 部分使函式庫更容易在穩定的 Rust 上工作並無縫支援在 nightly 版本使用 async/await。它還允許我們完成關於 API 部分的設計討論,並專注於在 async 語法穩定之前剩下的幾個問題。

歷史背景

這些被提議要穩定的 API 有著悠久的歷史:

  • Future 特徵從 futures crate 開始; 0.1 於 2016 年 8 月發布。該版本確立了「任務/輪詢」模型的核心思想,以及此處保留的 API 的許多其他方面。 0.1 系列持續在整個 Rust 生態系和正式系統中大量使用。

  • 2018 年初,隨著 async/await 的工作開始,futures 團隊建立了一個 RFC 流程並編寫了幾個 RFC,以根據長期的社群回饋對核心 API 進行修訂。這些 RFC 最終產生了 0.2le 版本,並於 4 月釋出

  • 在同一時期,@withoutboats 在支援 async 區塊內借用的 pinning API 上的工作已經完成pinning API 顛覆了過往一切,可在不使 future 的核心 API 成為 unsafe 前提下,支援 borrowing-across-yield。

  • 2018 年 4 月,一對 RFC 正式提出了 async/await 語法以及 futures API 的進一步修訂(以利用 pinning API 帶來的好處);後者經歷了許多修訂,包括一個新的 RFC。最終,語法 RFC 在 5 月被合併,而 API RFC 被關閉,進一步的設計疊代將在 nightly 版本進行,隨後是一個穩定的 RFC:這個!

  • 這些 API 在 5 月底加入到 std

  • 從那時起,隨著我們從 API 獲得的經驗,語法、std API 和 futures 0.3 crate 都在同步發展。這種體驗的主要驅動者是 Google 的 Fuchsia 專案,該專案在作業系統設置中大規模使用所有這些功能。

  • 最近的修訂是在 8 月,其中涉及一些關於如何使 Pin API 有更清晰的見解。這些 API 已被提議要穩定,以及它們作為 self 型別的用途

  • 有多個相容層可以同時使用 futures 0.1 和 0.3。這很重要,因為它允許現有生產程式碼的增量遷移。

自從最初的 futures 0.3 發布以來,除了上面提到的改進之外,核心 Future 特徵和任務系統的變化相對較小。實際的 Future 特徵基本上與 4 月時相同。

教學式解說

Future 特徵代表了一個非同步和惰性計算,它最終可能會產生一個最終值,但不以阻塞當前執行緒為前提來達成。

Future 可以通過 async 區塊或 async 函式來創建,例如

#![allow(unused)]
fn main() {
async fn read_frame(socket: &TcpStream) -> Result<Frame, io::Error> { ... }
}

當呼叫這個 async 函式,會產生一個 future,代表完成了從给定的 socket 中讀取一個框(frame)。函式簽名等同於:

#![allow(unused)]
fn main() {
fn read_frame<'sock>(socket: &'sock TcpStream)
    -> impl Future<Output = Result<Frame, io::Error>> + 'sock;
}

其他非同步函數可以 await 這個 future; 有關完整詳細信息,請參閱隨附的 RFC

除了 async fn 定義,future 還可以使用轉接器(adapter)來創建,就像 Iterator 一樣。最初,這些轉接器將完全由其他 crate 所提供,但最终它們將帶入標準函式庫。

最終非同步計算以任務的形式執行,相當於輕量級執行緒。 executor 提供了從 () 生成的 Future 創建任務的能力。執行器將固定 Future 並對其進行 poll,直到為其創建的任務完成。

executor 的實作以協同式地(cooperative)排程它的任務。是否使用一個或多個作業系統執行緒以及可以在其之上平行生成多少任務取決於 executor 的實作。一些 executor 的實作可能只能驅動單個 Future 完成,而另一些則可以提供動態接受新 Future 的能力,這些 Future 是在任務中被驅動完成。

該 RFC 不包含任何 executor 的定義。它僅以 API 的形式定義了 executors、tasks 和 Future 之間的交互關係,這些 API 允許任務請求再次進行排程。 這些 API 由 task 模組提供,在手動實作 Future 或 executor 時需要這些 API。

技術文件式解說

core::task 模組

Rust 中非同步運算的基本機制是任務,它們是輕量級的執行緒;許多任務可以協同調度到單個作業系統執行緒上。

為了執行這種協作調度,我們使用了一種有時被稱為「trampoline」的技術。當一個任務需要阻塞等待某個事件時,它會保存一個物件,允許它稍後再次被調度並返回到運行它的執行器,然後它可以執行另一個任務。隨後的喚醒將任務放回就緒任務的執行者隊列中,就像作業系統中的執行緒調度程序一樣。

嘗試完成一個任務(或其中的非同步值)稱為輪詢,並且總是返回一個 Poll 值:

#![allow(unused)]
fn main() {
/// Indicates whether a value is available, or if the current task has been
/// scheduled for later wake-up instead.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Poll<T> {
    /// Represents that a value is immediately ready.
    Ready(T),

    /// Represents that a value is not ready yet.
    ///
    /// When a function returns `Pending`, the function *must* also
    /// ensure that the current task is scheduled to be awoken when
    /// progress can be made.
    Pending,
}
}

當一個任務返回 Poll::Ready 時,執行器知道該任務已經完成並且可以被刪除。

喚醒

如果 future 在執行期間無法直接完成並返回 Pending,則它需要一種方法來稍後通知執行器它需要再次輪詢以取得進展。

此功能是透過一組 Waker 型別提供的。

Waker 是作為參數傳遞給 Future::poll 調用的物件,並且可以透過這些 Futures 的實現來儲存。每當一個 Future 需要再次被輪詢時,它可以使用喚醒器的 wake 方法來通知執行器擁有 Future 的任務應該被再次調度和執行。

RFC 定義了一個具體的 Waker 型別,Futures 和非同步函式的實作者將與之互動。此型別定義了一個 wake(&self) 方法,用於安排與 Waker 關聯的任務再次輪詢。

再次調度任務的機制取決於驅動任務的執行器。喚醒執行器的可能方法包含:

  • 如果執行器在條件變數上被阻塞,則需要通知條件變數。
  • 如果執行器在諸如 select 之類的系統調用上被阻塞,它可能需要被諸如 write 管道之類的系統調用喚醒。
  • 如果執行器的執行緒被放置,喚醒呼叫需要將其取消放置。

為了讓執行器實現自定義喚醒行為,Waker 型別包含一個稱作 RawWaker 的型別,它由指向自定義可喚醒物件的指針和對其提供 clonewakedrop 函式的虛擬函數指針表 (vtable) 的引用組成底層可喚醒物件。

選擇這種機制有利於特徵物件,因為它允許更靈活的記憶體管理方案。 RawWaker 可以單純根據全域函式和狀態、在引用計數物件之上或以其他方式實現。這種策略還可以更容易地提供不同的 vtable 函式,這些函式將執行不同的行為,儘管引用了相同底層可喚醒的物件型別。

這些 Waker 型別之間的關係在以下定義中進行了概述:

#![allow(unused)]
fn main() {
/// A `RawWaker` allows the implementor of a task executor to create a `Waker`
/// which provides customized wakeup behavior.
///
/// It consists of a data pointer and a virtual function pointer table (vtable) that
/// customizes the behavior of the `RawWaker`.
#[derive(PartialEq)]
pub struct RawWaker {
    /// A data pointer, which can be used to store arbitrary data as required
    /// by the executor. This could be e.g. a type-erased pointer to an `Arc`
    /// that is associated with the task.
    /// The value of this field gets passed to all functions that are part of
    /// the vtable as first parameter.
    pub data: *const (),
    /// Virtual function pointer table that customizes the behavior of this waker.
    pub vtable: &'static RawWakerVTable,
}

/// A virtual function pointer table (vtable) that specifies the behavior
/// of a `RawWaker`.
///
/// The pointer passed to all functions inside the vtable is the `data` pointer
/// from the enclosing `RawWaker` object.
#[derive(PartialEq, Copy, Clone)]
pub struct RawWakerVTable {
    /// This function will be called when the `RawWaker` gets cloned, e.g. when
    /// the `Waker` in which the `RawWaker` is stored gets cloned.
    ///
    /// The implementation of this function must retain all resources that are
    /// required for this additional instance of a `RawWaker` and associated
    /// task. Calling `wake` on the resulting `RawWaker` should result in a wakeup
    /// of the same task that would have been awoken by the original `RawWaker`.
    pub clone: unsafe fn(*const ()) -> RawWaker,

    /// This function will be called when `wake` is called on the `Waker`.
    /// It must wake up the task associated with this `RawWaker`.
    pub wake: unsafe fn(*const ()),

    /// This function gets called when a `RawWaker` gets dropped.
    ///
    /// The implementation of this function must make sure to release any
    /// resources that are associated with this instance of a `RawWaker` and
    /// associated task.
    pub drop_fn: unsafe fn(*const ()),
}

/// A `Waker` is a handle for waking up a task by notifying its executor that it
/// is ready to be run.
///
/// This handle encapsulates a `RawWaker` instance, which defines the
/// executor-specific wakeup behavior.
///
/// Implements `Clone`, `Send`, and `Sync`.
pub struct Waker {
    waker: RawWaker,
}

impl Waker {
    /// Wake up the task associated with this `Waker`.
    pub fn wake(&self) {
        // The actual wakeup call is delegated through a virtual function call
        // to the implementation which is defined by the executor.
        unsafe { (self.waker.vtable.wake)(self.waker.data) }
    }

    /// Returns whether or not this `Waker` and other `Waker` have awaken the same task.
    ///
    /// This function works on a best-effort basis, and may return false even
    /// when the `Waker`s would awaken the same task. However, if this function
    /// returns `true`, it is guaranteed that the `Waker`s will awaken the same task.
    ///
    /// This function is primarily used for optimization purposes.
    pub fn will_wake(&self, other: &Waker) -> bool {
        self.waker == other.waker
    }

    /// Creates a new `Waker` from `RawWaker`.
    ///
    /// The method cannot check whether `RawWaker` fulfills the required API
    /// contract to make it usable for `Waker` and is therefore unsafe.
    pub unsafe fn new_unchecked(waker: RawWaker) -> Waker {
        Waker {
            waker: waker,
        }
    }
}

impl Clone for Waker {
    fn clone(&self) -> Self {
        Waker {
            waker: unsafe { (self.waker.vtable.clone)(self.waker.data) },
        }
    }
}

impl Drop for Waker {
    fn drop(&mut self) {
        unsafe { (self.waker.vtable.drop_fn)(self.waker.data) }
    }
}
}

Waker 必須滿足以下要求:

  • 它們必須是可以克隆的。
  • 如果 Waker 的所有實體都已被刪除,並且它們的關聯任務已被驅動完成,則必須釋放為該任務分配的所有資源。
  • 即使關聯的任務已經被驅動完成,在 Waker 上調用 wake() 也必須是安全的。
  • Waker::wake() 必須喚醒執行器,即使它是從任意執行緒調用的。

因此,實例化 RawWaker 的執行程器必須確保滿足這些要求。

core::future 模組

有了上述所有任務的基礎設施,定義 Future 就很簡單了:

#![allow(unused)]
fn main() {
pub trait Future {
    /// The type of value produced on completion.
    type Output;

    /// Attempt to resolve the future to a final value, registering
    /// the current task for wakeup if the value is not yet available.
    ///
    /// # Return value
    ///
    /// This function returns:
    ///
    /// - [`Poll::Pending`] if the future is not ready yet
    /// - [`Poll::Ready(val)`] with the result `val` of this future if it
    ///   finished successfully.
    ///
    /// Once a future has finished, clients should not `poll` it again.
    ///
    /// When a future is not ready yet, `poll` returns `Poll::Pending` and
    /// stores a clone of the [`Waker`] to be woken once the future can
    /// make progress. For example, a future waiting for a socket to become
    /// readable would call `.clone()` on the [`Waker`] and store it.
    /// When a signal arrives elsewhere indicating that the socket is readable,
    /// `[Waker::wake]` is called and the socket future's task is awoken.
    /// Once a task has been woken up, it should attempt to `poll` the future
    /// again, which may or may not produce a final value.
    ///
    /// Note that on multiple calls to `poll`, only the most recent
    /// [`Waker`] passed to `poll` should be scheduled to receive a
    /// wakeup.
    ///
    /// # Runtime characteristics
    ///
    /// Futures alone are *inert*; they must be *actively* `poll`ed to make
    /// progress, meaning that each time the current task is woken up, it should
    /// actively re-`poll` pending futures that it still has an interest in.
    ///
    /// The `poll` function is not called repeatedly in a tight loop-- instead,
    /// it should only be called when the future indicates that it is ready to
    /// make progress (by calling `wake()`). If you're familiar with the
    /// `poll(2)` or `select(2)` syscalls on Unix it's worth noting that futures
    /// typically do *not* suffer the same problems of "all wakeups must poll
    /// all events"; they are more like `epoll(4)`.
    ///
    /// An implementation of `poll` should strive to return quickly, and must
    /// *never* block. Returning quickly prevents unnecessarily clogging up
    /// threads or event loops. If it is known ahead of time that a call to
    /// `poll` may end up taking awhile, the work should be offloaded to a
    /// thread pool (or something similar) to ensure that `poll` can return
    /// quickly.
    ///
    /// # Panics
    ///
    /// Once a future has completed (returned `Ready` from `poll`),
    /// then any future calls to `poll` may panic, block forever, or otherwise
    /// cause bad behavior. The `Future` trait itself provides no guarantees
    /// about the behavior of `poll` after a future has completed.
    ///
    /// [`Poll::Pending`]: ../task/enum.Poll.html#variant.Pending
    /// [`Poll::Ready(val)`]: ../task/enum.Poll.html#variant.Ready
    /// [`Waker`]: ../task/struct.Waker.html
    /// [`Waker::wake`]: ../task/struct.Waker.html#method.wake
    fn poll(self: Pin<&mut Self>, waker: &Waker) -> Poll<Self::Output>;
}
}

這裡的大部分解釋都遵循我們已經說過的關於任務系統的內容。一個轉折是 Pin 的使用,這使得可以在不同的 poll 調用中保留借用資訊(即「borrowing over yield points」)。固定機制在介紹它的 RFC 和有關最新修訂的部落格文章中進行了解釋。

與 futures 0.1 的關係

上面歷史背景部分概述的各種討論涵蓋了從 futures 0.1 到這些 API 的演進。但是,簡而言之,有三個主要轉變:

  • 使用 Pin<&mut self> 而不單純是 &mut self,這是支援借用 async 區塊所必需的。 Unpin 標記特徵可用於在手動撰寫 futures 時恢復類似於 futures 0.1 的人因工程和安全性。

  • Future 中刪除內置錯誤,以支援 Future 在可能失敗時返回 Result。 futures 0.3 crate 提供了一個 TryFuture 特徵,該特徵在 Result 中處理,以便在使用 Result-producer futures 時提供更好的人因工程。刪除錯誤型別已在之前的執行緒中討論過,但最重要的理由是為 async fn 提供一個正交的、組合性的語義,以反映正常的 fn,而不是採用特定的錯誤處理風格。

  • 顯式傳遞 Waker,而不是將其儲存在執行緒本地存儲中。自 futures 0.1 發布以來,這一直是一個備受爭議的問題,該 RFC 並不打算重新討論這個問題,但總而言之,主要優點是 (1) 在使用手動 futures(與 async 區塊相反)時,它更容易分辨哪裡需要環境任務,並且 (2) no_std 相容性明顯更好。

為了彌補 futures 0.1 和 0.3 之間的差距,有幾個相容性鋪墊,包括一個內置於 futures crate 本身的,您可以簡單地透過使用 .compat() 組合器在兩者之間切換。這些相容層使現有生態系可以順利地使用新的 futures API,並使大型程式的漸進式轉換成為可能。

原理、缺點和替代方案

該 RFC 是自 1.0 以來對 std 提出的最重要的補充之一。它讓我們在標準函式庫中包含一個特定的任務和輪詢模型,並將其與 Pin 聯繫起來。

到目前為止,我們已經能夠將「任務/輪詢」模型推向幾乎所有 Rust 希望佔據的利基點,而主要的缺點是缺乏 async/await 語法(以及它支援的借用)。

該 RFC 並未嘗試提供對源自 futures crate 任務模型的完整介紹。可以在以下兩篇部落格文章中找到有關設計原理和替代方案的更完整說明:

總而言之,futures 的主要替代模型是 callback-base 的方法,在發現當前方法之前嘗試了 callback-base 幾個月。根據我們的經驗,callback 方法在 Rust 中有幾個缺點:

  • 它幾乎在所有地方都強制分配(allocation),因此與 no_std 不相容。
  • 它使取消變得非常困難,而對於提議的模型,刪除只是「drop」。
  • 主觀上,組合器程式碼非常繁瑣,而基於任務的模型則可以輕鬆且快速達成。

附帶的 RFC 中提供了整個 async/await 專案的一些其他先備知識和基本原理。

在本節的其餘部分,我們將深入探討特定的 API 設計問題,其中該 RFC 與 futures 0.2 不同。

移除內嵌錯誤的基本原理、缺點和替代方法

在主要特徵中移除內嵌(built-in)錯誤型別有多種原因:

  • 改進的型別檢查和推斷。錯誤型別是當今使用 futures 組合器時最大的痛點之一,無論是試圖讓不同的型別配對,還是在一段程式碼無法產生錯誤時導致推理失敗。更明確地說,當 async 語法可用時,這些問題將不再那麼明顯。

  • 非同步函式。如果我們保留一個內嵌的錯誤型別,那麼 async fn 應該如何工作就不太清楚了:它是否應該總是要求回傳型別是一個 Result?如果不是,當回傳非 Result 型別時會發生什麼?

  • 組合器清晰度。按照組合器依賴錯誤與否來將其拆分,可闡明其語義。尤其對於 stream 來說更是如此,其中錯誤處理是常見的混淆來源。

  • 正交性(orthogonality)。一般來說,產生和處理錯誤與核心輪詢機制是分開的,所以在同等條件的情況下,遵循 Rust 的一般設計原則,藉由組合 Result 來處理錯誤似乎更好。

綜上所述,即使使用 TryFuture,對大量涉及錯誤處理的程式碼來說,仍有真正的缺點:

  • 需要額外的引入(如果程式碼引入 future prelude,則可以避免,我們也許可以更直接地鼓勵這麼做)。

  • 從程式碼角度來說,受一個特徵的約束實作另一個特徵可能會令人困惑。

該 RFC 的錯誤處理部分與其他部分是分開的,因此主要的替代方案是保留內嵌的錯誤型別。

核心特徵設計的基本原理、缺點和替代方案 (wrt Pin)

撇開上面討論過的錯誤處理之正交性不談,這個 RFC 中的另一主要項目是核心輪詢方法向 Pin 轉移,以及它與 Unpin/手動撰寫的 futures 之間的關係。在 RFC 討論過程中,我們基本上確定了解決這個問題的三種主要方法:

  • 一個核心特徵。這就是主要 RFC 文本中採用的方法:只有一個核心 Future 特徵,它適用於 Pin<&mut Self>。另外還有一個 poll_unpin 輔助器,用於在手動實現中使用 Unpin futures。

  • 兩個核心特徵。我們可以提供兩個特徵,例如 MoveFutureFuture,其中一個在 &mut self 上運行,另一個在 Pin<&mut Self> 上運行。這使得我們可以繼續以 futures 0.2 的風格編寫程式,即無需引入 Pin/Unpin 或以其他方式談論 pin。一個關鍵要求是需要可交互運作性(interoperation),以便可以在任何需要 Future 的地方使用 MoveFuture。至少有兩種方法可以實現這種互操作:

    • 透過對 T: MoveFutureFuture 全面實作(blanket implementation)。這種方法目前阻止了一些其他所需的實作(特別圍繞在 Box&mut),但這似乎不是問題的根本。

    • 透過子特徵的關係,使得 T: Future 本質上被定義為 for<'a> Pin<&mut 'a T>: MoveFuture 的別名。不幸的是,這種「更高等級」的特徵關係目前在特徵系統中效果不佳,而且這種方法在手動實現 Future 時也使事情變得更加複雜,收益相對較小。

該 RFC 採用的「一個核心特徵」方法的缺點是,它在手動編寫可移動 future 時與人因工程相衝突:您現在需要為您的型別引入 PinUnpin、調用 poll_unpin 和實現 Unpin。這一切都很機械化,但這是痛苦的。而 Pin 的人因工程學的改進可能會消除其中一些問題,但仍存在許多未解決的問題。

另一方面,雙特徵方法也有缺點。如果我們移除錯誤型別,則會出現組合爆炸,因為我們最終需要每個特徵的 Try 變體(這也延伸到相關的特徵,例如 Stream)。更廣泛地說,使用單一特徵方法,Unpin 充當一種「獨立旋鈕」,可以與其他關注點作正交應用;使用雙特徵方法,它是「混合的」。目前這兩種雙特徵方法都受到了編譯器的限制,儘管這不應該被視為決定因素。

該 RFC 選擇單一特徵方法的主要原因是它是保守的、前向相容(forward-compatible)的選擇,並且已經在實際中證明了自己。可以在未來的任何時候添加 MoveFuture 以及一個全面實作。因此,從本 RFC 中提出的單一 Future 特徵開始,在我們獲得經驗的同時,我們的選項也保持最大限度的開放。

喚醒設計 (Waker) 的基本原理、缺點和替代方案

該提議的先前疊代包括一個單獨的喚醒型別 LocalWaker,它是 !Send + !Sync 並且可用於實作最佳化的執行器行為,而無需原子引用計數或原子喚醒。然而,實際上,這些相同的最佳化可以使用執行緒本地喚醒佇列(queue)來實作,攜帶 ID 而不是指向喚醒物件的指標,並追蹤執行程序 ID 或執行緒 ID 以執行未發送 Waker 的 runtime 斷言執行緒。舉個簡單的例子,零原子的單執行緒鎖定執行器可以如下實現:

#![allow(unused)]
fn main() {
struct Executor {
    // map from task id (usize) to task
    tasks: Slab<Task>,
    // list of woken tasks to poll
    work_queue: VecDeque<usize>,
}

thread_local! {
  pub static EXECUTOR: RefCell<Option<Executor>> = ...;
}

static VTABLE: &RawWakerVTable = &RawWakerVTable {
    clone: |data: *const ()| RawWaker { data, vtable: VTABLE, },
    wake: |data: *const ()| EXECUTOR.borrow_mut().as_mut().expect(...).work_queue.push(data as usize),
    drop,
};
}

雖然此解決方案向 LocalWaker 方法提供了較差的錯誤訊息(因為在錯誤的執行緒上發生 wake 之前,它不会恐慌,而不是在 LocalWaker 轉換為 Waker 時發生恐慌),它極大地透過去除 LocalWakerWaker 型別的重複,簡化了給使用者的 API 。

在實際情況下,非常可能 Rust 生態系統中最常見的執行器繼續與多執行緒相容(就像現在一樣),因此在更特化的情況,針對這種情況最佳化其人因工程學,優先於更好的錯誤訊息。

現有技術

以 async/await 語法和以 future(又名 promise)為基礎的大量現有技術。提議的 futures API 是受到 Scala 的 futures 的影響,並且與各種其他語言的 API 大體相似(就以提供的轉接器而言)。

此 RFC 中模型的獨特之處在於使用了任務,而不是 callback。RFC 的作者不知道其他 future 函式庫是否使用這種技術,但它是 functional programming 中一種相當知名且更普遍的技術。有關於最近的範例,請參閱這篇關於 Haskell 平行的論文。這個 RFC 似乎是一个將 「trampoline」 技術與明確的、開放式的任務/喚醒模型结合起来的新想法。

未解決的問題

暫時沒有。

總結

可透過 HTTP 選擇性下載 crates-io 索引,類於 Ruby 的 Bundler 的作法。傳輸方式從預先(ahead-of-time)Git clone 改變為 HTTP 按需下載。既有的索引結構和內容維持不變。重要的是,此提案可與靜態檔案協同運作,而不需客製化的伺服器端 API。

動機

完整的 crate 索引相對龐大且下載慢。它還會隨著 crates.io 增長而增長,讓情況每況愈下。需要下載完整索引這件事拖累了首次使用 Cargo 的速度。尤其是在無狀態的 CI 環境下更是緩慢又浪費且只使用完整索引的非常小一部分,剩下全都給拋棄。快取索引在 CI 環境可謂困難重重(.cargo 目錄很巨)又經常低效(如:在 Travis Ci 上傳下載大型快取幾乎和下載新鮮索引一樣慢)。

目前索引的儲存資料格式和 git 協定搭配起來並不是很舒服。未壓縮的索引內容 tarball(截止 eb037b4863)佔 176MiB,gzip -1 需要 16MiB,而 xz -6 壓縮則要 10MiB。然 Git clone 卻回報下載了 215 MiB,比未壓縮的最新索引內容還大,且是壓縮後 tarball 的 二十倍之多

將 git 歷史記錄 shallow clone 或 squash 只是暫時解。且不論事實上 GitHub 指出它們不想支援大型儲存庫的 shallow clone,也不論 libgit2 尚未支援 shallow clone,實際上這作法仍未解決客戶端必須下載包含所有 crate 的整包索引。

教學式解說

將索引作為純文字檔案,透過 HTTP 開放出來。開放既有的索引佈局排列也夠用(就像從 raw.githubusercontent.com 看到的長相),不過以 HTTP 來說 URL scheme 可能可以簡化。

欲了解 crate 資訊和解析依賴,Cargo(或其他客戶端)可對每個依賴建立請求,並送往已知的 URL 以取得需要的資訊,例如:https://index.example.com/se/rd/serde。對各個依賴來說,客戶端也會繼續請求其依賴資訊, 遞迴循環,直到取得所有依賴(並快取)到本機端。

請求依賴檔案是有辦法平行化運作的,所以依賴解析最差情況下的延遲會限制在依賴樹的最大深度。實務上最差情況非常少見,因為依賴通常會多次出現在樹中,因此可以提前發現並增加平行化機會。且,若已有 lock 檔案,所有列在其中的依賴也都可平行化核對處理,

離線支援

此提議的解法完全保留了 Cargo 離線工作的功能。(連線的情況下)欲取得 crate 必然要下載足夠的索引來使用,之後所有資料會快取留作離線使用。

減少頻寬使用

Cargo 支援 HTTP/2,可有效率地處理多個相似的請求。

所有已獲取的依賴檔案可被快取,並透過條件式 HTTP 請求(EtagIf-Modified-Since 標頭)來避免重複下載未改變的檔案。

依賴的檔案易於壓縮,目前 rustc-ap-rustc_data_structures 內最大的檔案可用 Brotli 從 1MiB 壓縮到 26KiB。許多伺服器支援透明地提供預壓縮檔案(例如:可從帶有適當內容編碼標頭的 rustc-ap-rustc_data_structures.gz 提供 /rustc-ap-rustc_data_structures 的請求處理),因此索引可以保有高壓縮率但不必花費過多 CPU 以提供這些檔案。

就算是最糟的完整索引一個檔案一個檔案的下載狀況,比起 git clone 仍然使用更少頻寬(所有檔案分別壓縮加起來大小 39MiB)。

提供「增量式變更記錄」檔(在「未來展望」詳述)可以避免過多的條件式請求。

處理移除的 crate

有需要的話,本提議的解法可支援 crate 移除。當客戶端欲檢查一個已移除的 crate 的新鮮度,會向伺服器請求並獲得 404/410/451 HTTP 狀態。客戶端可以根據情況處理,並清理本機端的資料(甚至是 tarball 和原始碼已簽出的情況下)。

若客戶端對該移除 crate 不感興趣,就不會檢查之,但極大機率從來不會這樣做,也不會下載它。若主動清除已移除的 crate 快取資訊非常重要,則可以延伸「增量式變更記錄」來通知該移除。

缺點

  • crates-io 計畫替索引加上加密簽章,作為 HTTP 上額外一層保護。對 git 索引來說加密驗證非常直觀,不過簽署鬆散的 HTTP 索引可能蠻有挑戰性的。
  • 在還沒有增量式變更記錄前,一個基本的實作,需要許多請求來更新索引。和 git fetch 相比會有更高的延遲。然而,在一些早期的效能比較下,若 CDN 支援足夠(>60)多的平行請求,會比 git fetch 更快。對 Github 上的索引來說,Cargo 有一個捷徑會檢查 master 分支是否改變。若有增量式變更記錄檔,同樣的捷徑可以透過條件式 HTTP 請求該變更記錄檔來達成(例如,檢查 ETagLast-Modified)。
  • 此解法高效與否仰賴於建立多個平行小請求。支援 HTTP/2 的伺服器上做檢查是 HTTP/1.1 的兩倍快,但其實速度超越 HTTP/1.1 很合理。
  • 替代 registry 的功能目前已穩定,代表基於 git 索引協定也穩定且無法移除了。
  • 發送模糊搜尋索引的工具(例如 cargo add)可能需要發送數個請求,或是改用其他方法。URL 已全數正規化到小寫,所以不分大小寫的不需要額外的請求。

原理及替代方案

查詢 API

顯而易見的替代方案是建立可供查詢依賴解析的伺服器端 API(例如提供依賴清單就回傳一個 lockfile 或類似的東西)。然而,這會需要在伺服器端跑依賴解析。維護這種至關重要的動態 API 給幾乎所有 Rust 使用者每天使用,比其靜態檔案更難花費更高。

本提議之解法並不需要伺服器端任何邏輯。索引可以放在靜態檔案 CDN 上,且非常容易快取或鏡像。不需要修改索引如何取出。標準版本的索引可以繼續以 git 儲存庫形式維持完整的歷史記錄。這能保持與過往 Cargo 版本的相容性,並且第三方工具可以使用當前的索引格式。

從 rustup 提供初始索引

Rust/Cargo 的安裝可以打包初始版本的索引,這樣當 Cargo 運作時,就不需要從 git 下載整份索引檔,只需要做與種子版本差異更新。這個索引需要 rustup 更聰明地分別處理,避免在安裝或更新不同版本的 Cargo 時,造成重複下載索引多次。這會讓索引的下載和壓縮更好,讓做法可使目前的實作撐更久,但仍沒辦法解決索引不斷增長的問題。

本提議之解法更是伸縮自如,因為 Cargo 只要下載和快取「需要用到」的索引,未使用/棄用/垃圾 crate 不會造成任何花費。

Rsync

rsync 協定需要掃描並校驗原始碼與目標檔案的檢查碼異同,這需要許多不必要的 I/O,且需要 SSH 或在伺服器上運行的客製化系統服務(daemon),這限制了架設索引的選項。

先驅技術

https://andre.arko.net/2014/03/28/the-new-rubygems-index-format/

Bundler 曾經是預先下載完整索引,和 Cargo 一樣,直到它變得太大太大。然後它改用中心化的查詢 API,直到問題多到難以支援。然後就切換到增量式下載偏平檔案索引格式,和本提議解法相似。

未解決問題

  • 如何設定索引(包含替代 registry)該從 git 或新的 HTTP 下載?目前的語法使用 https:// URL 當作 git-over-HTTP。
  • 我們如何確保切換到 HTTP registry 不會導致 lock 檔案出現巨大差異?
  • 如何改寫當前的解析器,開啟平行取得索引檔案的功能?目前所有索引需要同時都可用,這杜絕了平行化的可能性。

實作可行性

目前已有一個經過測試的實作,是以簡單「貪婪」演算法來取得索引檔案,在 https://github.com/rust-lang/cargo/pull/8890 ,並且證實了不錯的效能,尤其是全新的建構。該實驗性實作的 PR 建議一個修改解析器的做法,以移除不必要的貪婪取得階段。

未來展望

增量式的 crate 檔案

Bundler 替每個獨立的依賴檔案使用僅允許附加(append-only)格式,以盡可能只增量下載新版本資訊。Cargo 的格式幾乎就是 append-only(除了 yank),所以如果單一檔案成長到是個問題,應該要版本修復它。然而,當前最大的 crate rustc-ap-rustc_data_structures 天天發佈新版本,每個版本增加 44 位元組(壓縮後),所以就算十年後它也才 190KB(壓縮後),看起來並沒有駭人到需要解決。

增量式變更記錄

截至目前,本方案在每次更新索引時,都必須重驗證每個索引檔案的新鮮度,即使許多檔案根本沒變。執行 cargo update 會導致索引更新,不過也有其他時機會導致更新,例如專案缺少 lockfile,或是添加了新的依賴。雖然 HTTP/2 pipeline 和條件式 GET 請求使得請求多個未改變檔案不錯有效率,但如果我們能避免那些無謂的請求,只請求有更改的檔案會更好。

一個解決方案是提供一個索引的概覽,讓客戶端可以快速確認本機端的索引檔是否過期。為了避免客戶端無謂的請求完整引樹快照,索引可以維護一個僅允許附加(append-only)的變更記錄。當改變發生(crate 版本發佈或 yank),記錄檔會附加新的一條記錄:epoch number(下面解釋之)、最後修改時間戳記、變更的 crate 名稱,未來需要也可添加其他額外資訊。

因為這個記錄檔只允許附加,所以客戶端可以利用 Range HTTP 請求漸增地更新它。客戶端不必下載完整的記錄檔來用,下載從任意一點到檔案結束這段即可,這用 Range 請求來達成簡單易懂。當在記錄裡(從最末端開始)找到一個 crate,並且其更動時間和本機快取一致,如此客戶端就不需要額外對該檔案發送 HTTP 請求。 當記錄檔成長過大,epoch number 遞增,記錄檔就可以重設到空白。即使在客戶端對該新記錄檔的 Range 請求合法的狀況下,epoch number 仍可讓客戶端有辦法偵測記錄檔是否有重設。

最終,這個 RFC 並沒有建議此方案,因為記錄檔恐導致程式碼變得十分複雜,且比起簡單的下載正面效益不大。若索引快照跟著 registry signing 一起實作了,此 RFC 的實作就可利用該快照機制當作變更記錄。

應付不一致的 HTTP 快取

索引並不需要將所有檔案合併產生一個快照。索引是一次更新一個檔案,並且只需保留部分更新排序。從 Cargo 的角度來看,各個依賴可允許獨立更新。

過期的快取僅在於新版本的 crate 使用了最新版本且剛發佈的依賴時,且該 crate 的快取失效得比其依賴早,才可能產生問題。Cargo 要求依賴需在可見的索引上有充分版本資訊,並且不會發佈任何「毀損」的 crate。

然而,CDN 快取總是有機會過期或失效的順序不一致。若 Cargo 偵測到索引的快取過期了(例如一個 crate 的依賴尚未出現在索引中),它可從該狀況復原,附加「快取破壞者」(如當前時間戳記)在 URL 上,以重新向索引請求檔案。就算 CDN 並不理會請求中的 cache-control: no-cache,這還是可靠能繞過過期快取的方法。

總結

棄用現有的 asm! 巨集(macro),並提供一個名為 llvm_asm! 的相同巨集(macro)。功能開關也從 asm 重新命名為 llvm_asm 。 與 asm! 不同的是, llvm_asm! 不打算成為穩定版。

動機

這個變更將 asm! 巨集(macro)釋放了,使它可用於內嵌組合語言項目組新設計的 asm! 巨集(macro), 同時為現在使用 asm! 的使用者提供一個簡單的方法來保持程式碼的正常運作。

對尚未支援新的 asm! 巨集(macro)的架構,在(nightly 版本)上執行內嵌組合語言可能會有用。

教學式解說

Rust 團隊目前正在重新設計 asm! 巨集(macro)。您應該將所有使用 asm! 的程式碼都替換為 llvm_asm! ,以避免程式碼在實施新的 asm! 巨集(macro)時毀壞。

技術文件式解說

編譯器內所有對 asm! 的參考都將改參考 llvm_asm!asm! 將變成一個單純(已棄用)的 macro_rules! ,它會重新導到llvm_asm!。 棄用警告將告知使用者 asm! 將來的語義會作改變,並邀請他們使用 llvm_asm! 來替代。llvm_asm! 巨集(macro)將由 llvm_asm 功能開關來把控。

缺點

此變更可能需要人為變更兩次程式碼:首先變更為 llvm_asm! ,然後再實施新的 asm! 巨集(macro)。

原理及替代方案

我們可以跳過棄用期,並同時執行重新命名新的 asm! 巨集(macro)。 總之用 Rust(nightly 版本)保證能一次破壞大量程式碼,就無需任何過渡期。

先驅技術

D 語言也支援兩種形式的內嵌組合語言。first one 提供用於內嵌組合語言的嵌入式 DSL,它可以在不用 Clobber 情況下直接存取範圍內的變數,但只能在x86和x86_64的架構上使用。 second one 是 LLVM 內部內嵌組合語言句法的 RAM 介面,但它只適用於 DSL 的後端架構。

未解決問題

未來展望

當下執行的會在 new asm! macro 被執行後替換掉,這會破壞那些尚未轉換 llvm_asm! 的程式碼。 由於運算元分隔符將從 : 更改為 , ,所以不會有靜默的錯誤編譯,新的 asm! 巨集(macro)會出現語法錯誤,來保證現有 asm! 的任何呼叫都會失敗,

總結

透過引進輸入輸出安全性(I/O safety)之概念與一系列新型別與特徵,保障 AsRaw 及相關特徵的使用者對原始資源 handle (raw resource handle)之使用,以彌補 Rust 封裝邊界的漏洞。

動機

Rust 標準函式庫幾乎算是已經提供了輸入輸出安全性,保證程式的一部分若私自持有一個原始 handle (raw handle),其他部分就無法存取。例如 FromRawFd::from_raw_fd 標示為不安全,它不允許使用者在 safe Rust 之下執行如 File::from_raw_fd(7) 等操作,或是在程式各處私自持有檔案描述子(file descriptor)來執行輸入輸出。

不過仍有漏網之魚。許多函式庫的 API 透過接受 AsRawFd/IntoRawFd 來執行輸入輸出操作:

#![allow(unused)]
fn main() {
pub fn do_some_io<FD: AsRawFd>(input: &FD) -> io::Result<()> {
    some_syscall(input.as_raw_fd())
}
}

AsRawFd 並無限制 as_raw_fd 的回傳值,所以 do_some_io 最終會對任意 RawRd 的值執行輸入輸出。由於 RawFd 本身實作了 AsRawFd,甚至可以寫出 do_some_io(&7)

這會使得程式存取錯誤的資源,更甚者為其他地方私有的原始 handle 建立多個別名(alias),從而打破封裝的邊界,造成遠處來的詭異行為

而在特殊情況下,違反輸入輸出安全性恐導致違反記憶體安全性。舉例來說,理論上替透過 Linux memfd_create 系統呼叫建立出來的檔案描述子打造 mmap 的安全封裝,並將之傳給 safe Rust 是可行的,畢竟它就是匿名的被開啟的檔案(anonymous open file),表示其他行程(process)無法存取之。然而,在沒有輸入輸出安全性且沒有永久封閉該檔案的情況下,該程式中其他程式碼恐意外地對該檔案描述子呼叫 writeftruncate,進而打破記憶體安全性 &[u8] 不變的規則。

這個 RFC 透過以下幾點,開闢一條逐步關閉此漏洞的道路:

  • 一個新概念:輸入輸出安全性。其概念會撰寫在標準函式庫文件中。
  • 一系列全新的型別與特徵。
  • from_raw_fd/from_raw_handle/from_raw_socket 撰寫新文件,解釋就輸入輸出安全性而言,它們為何不安全,順便解決出現好的相同問題。

教學式解說

輸入輸出安全性概念

Rust 標準函式庫提供了低階型別,以表示原始的作業系統資源 handle:類 Unix 平台的 RawFd 和 Windows 上的 RawHandleRawSocket。然而,它們並沒有提供任何自身的行為,而是僅作為一個標識符(identifier),並在低階的作業系統 API 間傳遞。

這些原始 handle 可以視為原始指標(raw pointer)且具有相同的危險性。雖然取得一個原始指標是安全的,但當該原始指標是非法指標,或是比其指向記憶體之處活得更久時,對原始指標取值(dereference)都可能引發未定義行為(undefined behavior)。無獨有偶,透過 AsRawFd::as_raw_fd 或類似方法取得一個原始 handle 是安全的,但當它並非合法 handle 或在關閉之後才拿來用時,用該 handle 來執行輸入輸出恐導致「損毀的輸出結果」、「遺失或洩漏輸入資料」或「違反封裝的邊界」。在這兩個案例中,影響不僅限於本地,也會影響程式的其他部分。保護原始指標免於危險稱作記憶體安全性,所以保護原始 handle 免於危險我們叫它輸入輸出安全性

Rust 標準函式庫也有高階型別如 FileTcpStream 來提供高階的作業系統 API 界面,這些型別包裝了原始 handle。

這些高階型別同時實作了在類 Unix 平台的 FromRawFd 特徵,以及 Windows 上的 FromRawHandle/FromRawSocket 。這些特徵提供許多函式將底層的值封裝產生出高階的值。由於這些函式無法確保輸入輸出的安全性,因此標示為不安全。型別系統並不會限制這些型別傳入:

#![allow(unused)]
fn main() {
    use std::fs::File;
    use std::os::unix::io::FromRawFd;

    // 建立一個檔案。
    let file = File::open("data.txt")?;

    // 從任意整數值構建一個 `File`,這個型別能通過檢查,但 7 可能在執行期間無法
    // 被識別為任何活生生的資源,或它可能不慎指向程式其他地方封裝好的原始 handle
    // 一個 `unsafe` 區塊告知呼叫者對此有責任,需使其免於這些風險。
    let forged = unsafe { File::from_raw_fd(7) };

    // 取得一個 `file` 內部的原始 handle 的副本。
    let raw_fd = file.as_raw_fd();

    // 關閉 `file`。
    drop(file);

    // 開啟一些無關的檔案。
    let another = File::open("another.txt")?;

    // 其他對 `raw_fd`(也就是 `file` 內部的原始 handle)的使用有可能使其生命週
    // 期長於與作業系統的關聯。這恐導致意外建立別名指向其他封裝起來的 `File`
    // 實例,例如 `another`。因此,一個 `unsafe` 區塊告知呼叫者對此有責任,需使
    // 其免於這些風險。
    let dangling = unsafe { File::from_raw_fd(raw_fd) };
}

呼叫端必須確保傳入 from_raw_fd 的值一定是從作業系統回傳所得,並且 from_raw_fd 的回傳值的生命週期不會長於和作業系統關聯的 handle。

雖然將輸入輸出安全性作為明確的概念是個新作法,但其實它也反映出許多常見的實踐。除了引入一些新型別與特徵及其實作外,Rust std 並不需改變已經穩定的界面。在推行之初,並不需讓整個 Rust 生態系馬上支援輸入輸出安全性,而是可以漸進式地採用之。

OwnedFdBorrowedFd<'fd>

這兩個型別概念上將取代 RawFd,並分別表示擁有和借用的 handle 值。OwnedFd 擁有一個檔案描述子,當 OwnedFd 釋放時就會關閉其檔案描述子。BorrowedFd 的生命週期標示該檔案描述子被借用多久。這些型別皆會自動實施其輸入輸出安全性不變的規則。

至於在 Windows 上,會以 HandleSocket 形式呈現相應的型別。

這些型別在輸入輸出扮演的角色可類比 Rust 既有的記憶體管理相關型別:

型別類似於
OwnedFdBox<_>
BorrowedFd<'a>&'a _
RawFd*const _

不過兩者還是有差,輸入輸出安全性並不區分可不可變。在 Rust 的掌控之外,作業系統資源能以各種形式共享,所以輸入輸出可以視為使用了內部可變性

AsFdInto<OwnedFd>From<OwnedFd>

這三個型別概念上,在大多數用例中分別取代 AsRawFd::as_raw_fdIntoRawFd::into_raw_fd,以及 FromRawFd::from_raw_fd。它們依據 OwnedFdBorrowedFd 來運作,所以也會自動實施其輸入輸出安全性不變的規則。

使用這些型別後,就能避免在動機一節的 do_some_io 範例中提及的問題。由於只有合理擁有或借用檔案描述子的型別能實作 AsFd,所以這個版本的 do_some_io 不需要擔心偽造或迷途(dangling)的檔案描述子傳入。

#![allow(unused)]
fn main() {
pub fn do_some_io<FD: AsFd>(input: &FD) -> io::Result<()> {
    some_syscall(input.as_fd())
}
}

至於在 Windows 上,會以 HandleSocket 形式呈現相應的型別。

漸進式採用

輸入輸出安全性及其新型別與新特徵並不需要一次性全面導入,而是能夠分階段漸進採用:

  • 首先,在 std 新增這些新型別與新特徵,並替相關的型別實作之。這是向下相容的改變。
  • 在此之後,crate 可以開始用這些新型別,並替 crate 自己的型別實作這些新特徵。這些改變相對小,而且符合語意化版號相容性,不需其他特殊處理。
  • 當標準函式庫和夠多的熱門 crate 實作這些新特徵後,其他 crate 可按照它們的開發步調,將這些新特徵作為泛型引數的限定條件(bound)。雖然這些改變不符合語意化版號的相容性,不過多數 API 使用者改用新特徵時並不需改變程式碼。

技術文件式解說

輸入輸出安全性概念

Rust 語言除了有記憶體安全性之外,Rust 標準函式庫同時提供了對輸入輸出安全性的保證。一個合法的輸入輸出操作,其所操作的原始 handle(RawFdRawHandleRawSocket)必為明確從作業系統回傳之值,且這些操作僅發生在與作業系統關聯之生命週期內。當一段 Rust 程式碼宣稱輸入輸出安全性,代表該程式碼不可能導致非法的輸入輸出操作。

雖然有些作業系統的文件中說明其檔案描述子的配置演算法,但從這些演算法旁敲側擊出來的 handle 值並不會視為「明確從作業系統回傳之值」。

對接受任意原始輸入輸出 handle 值(RawFdRawHandleRawSocket)的函式,若安全的 API 會藉由這些 handle 執行輸入輸出,應標示為 unsafe

OwnedFdBorrowedFd<'fd>

OwnedFdBorrowedFd 皆為 repr(transparent),並帶有一個 RawFd 值,且兩者皆可應用區位最佳化(niche optimizations),所以和 Option<OwnedFd>Option<BorrowedFd<'_>> 的大小相同,而且可在 FFI 宣告的函式中使用,例如 open, read, write, close 等。若以上述方法使用之,它們將確保 FFI 邊界的輸入輸出安全性。

這些型別同時會實作既有的 AsRawFdIntoRawFd,以及 FromRawFd 特徵,所以它們可和既有程式碼的 RawFd 型別交互使用。

AsFdInto<OwnedFd>From<OwnedFd>

這些型別提供 as_fdintofrom 函式,類似於 AsRawFd::as_raw_fdIntoRawFd::into_raw_fdFromRawFd::from_raw_fd

原型實作

上述所有原型放在:

https://github.com/sunfishcode/io-lifetimes

README.md 有文件鏈結、範例、和當前提供類似功能的 crate 之調查研究。

缺點

Crate 若用到檔案描述子,如 nixmio,將需要遷移到有實作 AsFd 的型別,或將這類函式標示為不安全。

crates 若用 AsRawFdIntoRawFd 來接收任何「類檔案」或「類 socket」型別,如 socket2SockRef::from,將需換成 AsFdInto<OwnedFd>,或將這類函式標示為不安全。

原理及替代方案

有關「unsafe 僅為了記憶體安全性」

Rust 有個慣例:unsafe 只適用於標示記憶體安全性。舉個有名的案例, std::mem::forget 曾標示為不安全,但後來被改為安全,且其結論指出 unsafe 僅該用作標示記憶體安全性,而不該用來標示可能作繭自縛的情況或恫嚇應避免使用的 API 上。

記憶體安全性造成的危害比其他程式設計面向更甚,它不僅要避免意外行為發生,且仍需避免無法限制一段程式碼能做什麼。

輸入輸出安全性也落在這個範疇,有兩個因素:

  • (若作業系統存在 mmap 相關 API)在安全封裝的 mmap 中,輸入輸出安全性的錯誤仍會導致記憶體安全性的錯誤。
  • 輸入輸出安全性之錯誤也意味著一段程式碼可以在沒有通知或被授予任何引用的情形下,讀寫或刪除程式其他部分正在使用的資料。這使得在無法通曉一個 crate 鏈結到的其他 crate 的所有實作細節下,非常難以限制這個 crate 可以做什麼。

原始 handle 更像指向單獨的位址空間(address space)的原始指標,它們可能迷途(dangle)或造假。輸入輸出安全性近似於記憶體安全性,兩者皆竭力杜絕遠處來的詭異行為(spooky-action-at-a-distance),且對兩者來說,所有權都可作為建立穩健抽象化的主要根基,所以自然而然共用了相似的安全性概念。

將輸入輸出 Handle 當作純資料

主要的替代方案的說法是原始 handle 為純資料(plain data),並沒有輸入輸出安全性,和作業系統資源的生命週期也無任何與生俱來的關係。至少在類 Unix 平台,這些永遠不會導致記憶體不安全或是未定義行為。

不過,大部分的 Rust 程式碼不直接與原始 handle 互動。撇開本 RFC 不談,不與原始 handle 互動是件好事。所謂資源,一定會有生命週期,若大部分的 Rust 程式碼能使用各方面都更易上手又能自動管理生命週期的更高階的型別,這樣鐵定更棒。不過,純資料的方案對於相對不常見的案例,最多只能讓原始 handle 的操作容易撰寫些。這可能只是蠅頭小利,甚至可能是個缺點,有可能最後變相鼓勵大家在不需要時去用了原始 handle 。

純資料的方案亦不需要變更任何 crate 的程式碼。而輸入輸出安全性的方案則需改動如 socket2nixmio 這些用到 AsRawFdRawFd 的 crate,不過這改動可以漸進推廣到整個生態系,而不必一次性完成。

IoSafe 特徵(與它的前身 OwnsRaw

這個 RFC 在早先幾個版本提議過一個 IoSafe 特徵,這個特徵會帶來小程度但具侵入性的修復。來自該 RFC 的回饋促使一系列新型別與特徵的開發。這個開發牽扯更廣的 API 範圍,也意味著需要更多設計和審核。並且隨著時間推移,整個 crate 生態系需要更大規模的改動。然而,早期跡象指出,本 RFC 引入的新型別與特徵更易理解,使用上更順手且安全,所以長期來說有更穩健的基礎。

IoSafe 早期叫做 OwnsRaw。我們很難替這個特徵找到恰到好處的名字,這也許是個訊號,表示它並非良好設計的特徵。

先驅技術

大部分記憶體安全的程式語言都對原始 handle 做了安全的抽象層。多數情況下,它們僅是簡單地避免暴露原始 handle,例如 C#Java 等。若將透過原始 handle 執行輸入輸出標示為不安全,可讓 safe Rust 與這些程式語言達到相同程度的安全保證。

在 crates.io 上有好幾個 crate 封裝了擁有或借用的檔案描述子。io-lifetimes 的 README.md 中 Prior Art 一節詳細描述了其與其他既有 crate 的同異。從高層次角度來看,既有的 crate 都與 io-lifetimes 共享相同的基本概念。這些 crate 都圍繞著 Rust 生命週期與所有權概念打造,而這恰恰說明這些概念非常適合這個問題。

Android 有特殊的 API 會偵測不恰當的 close,詳見 rust-lang/rust#74860。這些 API 的動機就是一種輸入輸出安全性的應用。Android 的特殊 API 用了動態檢查,讓它們可以在跨來源語言的邊界實施這些規則。本 RFC 提議的輸入輸出安全性的型別和特徵只專注在 Rust 程式碼本身實施這些規則,所以它們可在編譯期間利用 Rust 的型別系統實施這些規則,而非延遲到執行期間。

未解決問題

形式化所有權

此 RFC 並沒有為原始 handle 的所有權和生命週期定義一個形式化模型(formal model)。這 RFC 對原始 handle 規範之定位尚不明朗。當 handle 只是整數型別時,與其關聯資源之生命週期意義為何?所有具有相同值的整數型別會共享該關連嗎?

Rust 參考手冊根據 LLVM 的指標別名規則定義了記憶體的未定義行為;輸入輸出可能需要類似的 handle 別名規則。這對目前的實際需求而言似非必要,但未來可對此進行探索。

未來展望

以下包含一些從此 RFC 延伸的可能想法:

  • Clippy 警吿常見的輸入輸出不安全狀況。

  • 一個原始 handle 所有權形式化模型。可想像為延伸 Miri 使其揪出「關閉後使用」和「使用偽造的 handle」這類錯誤。

  • 一個屬於 Rust,細緻且基於能力的安全模型(capabability-based security model)。藉由此模型提供的保證,在 safe Rust 語境下就不可能偽造出假的原始 handle 高階封裝。

  • 還可替有實作 AsFdInto<OwnedFd>From<OwnedFd> 的型別添加一些方便的功能:

    • from_into_fd 函式:取得 Into<OwnedFd> 並將之轉為 From<OwnedFd>,讓使用者一步執行這些常見的轉換步驟。
    • as_filelike_view::<T>() 函式:回傳一個 View,其中包含內部檔案描述子構建出來的暫時實例 T,讓使用者能以 FileTcpStream 等方式查看原始的檔案描述子。
  • 簡單使用情景的可攜性。由於 Windows 有兩種不同的 handle 型別,但 Unix 只有一種,因此在這領域中達成可攜性並非易事。然而,在部分案例中,可將 AsFdAsHandle 一視同仁,而另外一些情況則可以把 AsFdAsSocket 當作相同的。在這兩類情形,普通的 FileLikeSocketLike 抽象化能讓程式碼泛用在 Unix 和 Windows 上。

    類似的可攜性也能推廣到 From<OwnedFd>Into<OwnedFd>

致謝

感謝 Ralf Jung (@RalfJung) 引導我理解這個主題至此,鼓勵我和審核這個 RFC 的草案,並耐心回答我諸多問題!

概要

在標準函式庫中新增作用域執行緒,其允許產生可借用父執行緒之變數的執行緒。

範例:

#![allow(unused)]
fn main() {
let var = String::from("foo");

thread::scope(|s| {
    s.spawn(|_| println!("borrowed from thread #1: {}", var));
    s.spawn(|_| println!("borrowed from thread #2: {}", var));
});
}

動機

在 Rust 1.0 發布之前,我們有與作用域執行續相同作用的 thread::scoped(),但後來發現一個健全性的問題,可能導致 use-after-frees,所以它已被移除。這一歷史事件稱為洩密事件

幸運的是,舊的作用域執行緒可被修復,透過閉包(closure)而非守護(guard),來確保生成的執行緒會自動會合(join)。但我們對在 Rust 1.0 中加入的作用域執行緒並不放心,所以我們決定將其放在外部 crates 之中,並有可能在未來的某個時刻回到標準函式庫中。四年過去了,那個未來就是現在。

作用域執行緒在 Crossbeam 中經過多年的經驗累積,我們的設計已經趨於成熟,足以被推廣到標準函式庫中。

更多內容請看基本理由及替代方案部分。

教學式解說

執行緒生成的 "hello world" 可能如下所示:

#![allow(unused)]
fn main() {
let greeting = String::from("Hello world!");

let handle = thread::spawn(move || {
    println!("thread #1 says: {}", greeting);
});

handle.join().unwrap();
}

現在讓我們嘗試生成兩個使用相同 greeting 的執行緒。不幸的是,我們必須克隆它,因為 thread::spawn()F: 'static 要求,這意味著執行緒不能借用局部變數:

#![allow(unused)]
fn main() {
let greeting = String::from("Hello world!");

let handle1 = thread::spawn({
    let greeting = greeting.clone();
    move || {
        println!("thread #1 says: {}", greeting);
    }
});

let handle2 = thread::spawn(move || {
    println!("thread #2 says: {}", greeting);
});

handle1.join().unwrap();
handle2.join().unwrap();
}

作用域執行緒來解決!透過打開一個新的 thread::scope() 區塊,我們可以向編譯器證明在這作用域內產生的所有執行緒也會在這個作用域內結束生命。

#![allow(unused)]
fn main() {
let greeting = String::from("Hello world!");

thread::scope(|s| {
    let handle1 = s.spawn(|_| {
        println!("thread #1 says: {}", greeting);
    });

    let handle2 = s.spawn(|_| {
        println!("thread #2 says: {}", greeting);
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
});
}

這意味著可以毫無顧忌地借用作用域之外的變數!

現在我們不必再手動會合執行緒,因為所有未會合的執行緒將在作用域結束時自動會合:

#![allow(unused)]
fn main() {
let greeting = String::from("Hello world!");

thread::scope(|s| {
    s.spawn(|_| {
        println!("thread #1 says: {}", greeting);
    });

    s.spawn(|_| {
        println!("thread #2 says: {}", greeting);
    });
});
}

當以這種方式利用自動會合時,請注意,如果任何自動會合的執行緒出現恐慌,thread::scope() 將出現恐慌。

你可能已經注意到作用域執行緒現在只接受一個參數,它只是對 s 的另一個引用。由於 s 存在於作用域內,我們不能直接借用它。使用傳遞的參數來生成巢狀執行緒:

#![allow(unused)]
fn main() {
thread::scope(|s| {
    s.spawn(|s| {
        s.spawn(|_| {
            println!("I belong to the same `thread::scope()` as my parent thread")
        });
    });
});
}

技術文件式解說

我們在 std::thread 模組中新增兩個新的型別:

#![allow(unused)]
fn main() {
struct Scope<'env> {}
struct ScopedJoinHandle<'scope, T> {}
}

生命週期 'env 代表作用域外的環境,而 'scope 代表作用域本身。更準確地說,作用域外的所有內容都比 'env'scope 內的所有內容都長。生命週期的關係是:

'variables_outside: 'env: 'scope: 'variables_inside

接下來,我們需要 scoped()spawn() 函式:

#![allow(unused)]
fn main() {
fn scope<'env, F, T>(f: F) -> T
where
    F: FnOnce(&Scope<'env>) -> T;

impl<'env> Scope<'env> {
    fn spawn<'scope, F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T>
    where
        F: FnOnce(&Scope<'env>) -> T + Send + 'env,
        T: Send + 'env;
}
}

這就是作用域執行緒的要點,真的。

現在我們只需要再做兩件事來使 API 完整。首先,ScopedJoinHandle 等同於 JoinHandle,但與 'scope 生命週期掛勾,所以它將有同樣的方法。第二,執行緒生成器需要能夠在一個作用域内生成執行緒。

#![allow(unused)]
fn main() {
impl<'scope, T> ScopedJoinHandle<'scope, T> {
    fn join(self) -> Result<T>;
    fn thread(&self) -> &Thread;
}

impl Builder {
    fn spawn_scoped<'scope, 'env, F, T>(
        self,
        &'scope Scope<'env>,
        f: F,
    ) -> io::Result<ScopedJoinHandle<'scope, T>>
    where
        F: FnOnce(&Scope<'env>) -> T + Send + 'env,
        T: Send + 'env;
}
}

缺點

作用域執行緒的主要缺點是使標準函式庫有點大。

基本原理和替代方案

  • 將作用域執行緒保留在外部 crates 之中。

    將他們放在標準函式庫有幾個優點:

    • 這是一個非常常見和實用的工具,非常適合學習、測試和探索性程式設計。每個學習 Rust 的人都會在某個時候遇到借用和執行緒的互動。有一個非常重要的教訓是執行緒實際上可以借用局部變數,但標準函式庫並沒有反映這一點。

    • 有些人可能會爭辯說我們應該完全不鼓勵使用執行緒,而是將人們指向像 Rayon 和 Tokio 這樣的執行器。但是,thread::spawn() 需要 F: F: 'static 並且無法繞過它,這感覺就像是標準函式庫中缺少的部分。

    • 實現作用域執行緒非常難處理,因此最好有標準函式庫提供一個可靠的解決方案。

    • 官方文檔和書籍中有許多範例可以透過作用域執行緒進行簡化。

    • 作用域執行緒通常是比 thread::spawn() 更好的預設值,因為它們確保生成的執行緒被連接並且不會意外 「洩漏」。這有時是單元測試中的一個問題,如果單元測試產生執行緒並忘記會合它們,「迷途」 的執行緒可能會積累。

    • 使用者一直在 IRC 和論壇上詢問作用域執行緒。將它們作為 std::thread 中的「祝福」模式對每個人都有好處。

  • scope 回傳一個 Result,包括所有捕獲的恐慌。

    • 這很快變得複雜,因為多個執行緒可能已經恐慌。回傳 Vec 或其他恐慌的集合並不總是最有用的介面,而且通常是不必要的。如果使用者想要處理它們,在 ScopedJoinHandle 上顯式使用 .join() 來處理恐慌是最靈活且最有效的方法。
  • 不要將 &Scope 參數傳遞給執行緒。

    • 假如使用 scope.spawn(|| ..) 而不是 scope.spawn(|scope| ..),則需要 move 關鍵字 (scope.spawn(move || ..)),如果你想在 closure 內使用作用域,將變得不符合人因工程。

現有技術

自 Rust 1.0 以來,Crossbeam 就有了作用域執行緒

Crossbeam 的作用域執行緒有兩種設計。舊的是在 thread::scoped() 被刪除後,我們想在 Rust 1.0 時代有一個合理的替代方案。新的則是在去年的大改版之中。

  • 舊: https://docs.rs/crossbeam/0.2.12/crossbeam/fn.scope.html
  • 新: https://docs.rs/crossbeam/0.7.1/crossbeam/fn.scope.html

新舊作用域執行緒之間存在一些差異:

  1. scope() 現在從子執行緒傳播未處理的恐慌。在舊的設計中,恐慌被默默地忽略了。使用者仍然可以透過手動操作 ScopedJoinHandle 來處理恐慌。

  2. 傳遞給 Scope::spawn() 的 closure 現在需要一個 &Scope<'env> 參數,該參數允許生成巢狀執行緒,這在舊設計中是不可能的。 Rayon 類似地傳遞了對子任務的引用。

  3. 我們刪除了 Scope::defer(),因為它不是真的有用,有錯誤,並且有不明顯的行為。

  4. ScopedJoinHandle'scope 上進行了參數化,以防止它逃離作用域。

Rayon 也有作用域,但它們在不同的抽象級別上工作——Rayon 產生任務而不是執行緒。它的 API 與本 RFC 中提出的 API 相同。

未解決的問題

這個概念可以延伸到非同步嗎?會有任何行為或 API 差異嗎?

未來的可能性

在未来,我們也可以有一個像 Rayon 那樣的執行緒池,可以生成作用域内的任務。

摘要

This RFC establishes a Leadership Council as the successor of the core team1 and the new governance structure through which Rust Project members collectively confer the authority2 to ensure successful operation of the Project. The Leadership Council delegates much of this authority to teams (which includes subteams, working groups, etc.3) who autonomously make decisions concerning their purviews. However, the Council retains some decision-making authority, outlined and delimited by this RFC.

The Council will be composed of representatives delegated to the Council from each top-level team.

The Council is charged with the success of the Rust Project as a whole. The Council will identify work that needs to be done but does not yet have a clear owner, create new teams to accomplish this work, hold existing teams accountable for the work in their purview, and coordinate and adjust the organizational structure of Project teams.

大綱

參考資料

為了縮短此 RFC 的篇幅,不具約束性質的參考資料將以單獨文件形式呈現:

動機

The Rust project consists of hundreds of globally distributed people, organized into teams with various purviews. However, a great deal of work falls outside the purview of any established team, and still needs to get done.

Historically, the core team both identified and prioritized important work that fell outside of team purviews, and also attempted to do that work itself. However, putting both of those activities in the same team has not scaled and has led to burnout.

The Leadership Council established by this RFC focuses on identifying and prioritizing work outside of team purviews. The Council primarily delegates that work, rather than doing that work itself. The Council can also serve as a coordination, organization, and accountability body between teams, such as for cross-team efforts, roadmaps, and the long-term success of the Project.

This RFC also establishes mechanisms for oversight and accountability between the Council as a whole, individual Council members, the moderation team, the Project teams, and Project members.

理事會之職責、期待與限制

高層級描述下,理事會負責以下職責:

  • 識別、優先排序以及追蹤由於缺乏明確負責者而無法執行的工作(而不應對負責者刻意降低優先度、置於待辦事項等的工作)。
  • 委派該工作,並潛在可建立全新的(並可能為暫時性的)團隊來負責此工作。
  • 為沒有明確負責者的緊急事項下達決策。
    • 此行為只能在特例情境下執行,即該決策無法委派給既有團隊也無法交付給全新團隊時。
  • 為團隊、架構或流程協調橫跨專案的變更。
  • 確保一級團隊能為其權責、對其它團隊以及對專案本身負責。
  • 確保各個團隊能有完成其工作所需的人力與資源。
  • 為 Rust 專案整體建立起官方的定位、觀點或意願。
    • 藉此能幫助降低橫跨專案的協調需求,尤其是面對長期公開投票與共識建立流程不夠實際的情境。舉例來說,要對第三方單位溝通 Rust 專案整體「 所想要的目標」時。

除了這些職責外,對理事會也有額外的期待與限制,以幫助判斷理事會是否正常運作:

  • 委派工作:理事會不應該主動執行本案沒有明確指派的工作;理事會必須委派給既有團隊或者成員與理事會不重複的全新團隊。執行工作的團隊中可以有理事會的代表,但是做為團隊成員並不屬於理事會代表的權責。
  • 為了確保專案長期的流暢運作:理事會應該要確保非緊急專案管理工作有接受優先度排序,並且能定期完成以確保專案不會累積成整個組織級別的負擔。
  • 負責任:由於理事會擁有廣泛的權力,理事會與理事會代表必須要為自己的行為負責任。他們應該要傾聽他人的回饋,並且能對他們是否可以繼續達到該職位所需的職責與期待進行主動的反思。
  • 做好代表:理事會代表不只要能代表整個專案各形式的考量,也要能盡可能代表 Rust 社群的各方面(人口分布、技術背景等等)。
  • 共享負擔:所有理事會代表都必須共享理事會職責的負擔。
  • 尊重他人的權責:理事會必須尊重委派給各團隊的權責。理事會必須與各團隊諮商並且共同合作解決問題,並且盡量不作出違背任何團隊意願的決策。
  • 以良善原則行事:理事會代表應該以 Rust 專案整體的利益作出決策,即便這些決策可能會與各個團隊、雇主以及其他外部單位的利益衝突。
  • 保持透明:雖然不是所有決策(以及決策的各方面)都能被公開,理事會應該要盡可能維持其決策的公開與透明。理事會也應該要確保專案的組織架構很明確透明。
  • 尊重隱私:理事會的成員絕不能為了透明性而洩漏個人或者機密情報,包含可能會意外洩漏重要資訊的周邊資訊。
  • 維持健康的工作環境:理事會代表應該要為他們的貢獻與其性質感到滿意。他們不應認為自己在理事會中的存在只是義務所為,而必須是因為他們用有意義的方式主動參與。
  • 持續演化:理事會受到期待會隨著時間演化以應對團隊、專案以及社群的演化。

理事會代表、審核團隊成員以及其它專案成員都應該能為身邊的人以及更廣泛的社群做好示範。這些職位都有對應的責任與領導,也因此這些人的行為都帶有份量而會對社群造成重大影響,也因此必須注意行使職權。選擇要行使這些職權的人都應該要知道身邊的人會以對應的高標準看待他們。

理事會組織架構

理事會由一群團隊代表所組成,各代表一個一級團隊以及其子團隊。

每支一級團隊都能用各自所選的程序選出恰好一名代表。

任何一級團隊的成員或其子團隊的成員都能作為其代表。團隊必須提供子團隊成員對潛在候選給予意見與回饋的經驗。

各個代表最多代表一個一級團隊,就算他們也是其它團隊的成員。代表任何 Rust 團隊的主要責任將由其所屬的一級團隊代表負責。4

所有 Rust 專案團隊都必須至少隸屬於一個一級團隊。對於目前沒有母團隊的團隊,本案會建立起「啟動台團隊」作為暫時性的隸屬對象。以確保所有團隊都在理事會有對應的代表。

一級團隊單位

理事會將會透過公開政策決策建立一級團隊。一般來說,一級團隊應該要符合以下條件:

  • 有對 Rust 專案具備根本重要性的權責
  • 是該權責全面向的最終決策者
  • 所有權責不隸屬於其它團隊權責(不能為子團隊或相似管理架構所有)
  • 有著能期待無止盡持續的開放權責
  • 目前為 Rust 專案中活躍的存在

一級團隊的數量必須介於 4 到 9(包含)之間,傾向介於 5 到 8 之間。這個數字能相對平衡多元性的需求以及相對小的架構,同時又在進行有效對話與共識凝聚時具備實務性。5

當理事會建立起全新一級團隊時,該團隊則該指派一名理事會代表。6當建立起全新的一級團隊時,理事會必須提供為何該一級團隊不能作為子團隊或者其它管理架構存在的解釋。

一級團隊初始名單

一級團隊的初始名單由本案使發表時列於 rust-lang.org 網站的一級管理區域的所有團隊組成(除去 Core 團隊與校友),再加上「啟動台」團隊

  • 編譯器
  • Crates.io
  • 開發工具
  • 基礎架構
  • 語言
  • 啟動台
  • 函式庫
  • 審核
  • 發佈

本清單並不是最佳的一級團隊組成。本案建議理事會的最開始的工作就是檢視既存的管理架構並確保所有架構都在一個或多個一級團隊中有直接或間接的代表,並且確保所有一級團隊都有符合能被認定為一級團隊的條件。這將會涉及對一級團隊組成的調整。

「啟動台」一級團隊

本案會建立「啟動台(launching pad)」團隊以暫時接受沒有對應一級團隊可以歸類的子團隊。這樣能夠確保在更加永久性的母團隊被找到或者建立起之前,所有團隊都在理事會有代表。

「啟動台」是個雨傘團隊:該團隊沒有直接成員,只有子團隊代表。

理事會應該要致力於為所有「啟動台」子團隊找尋或者建立更合適的母團隊,並且隨之將子團隊移轉至對應的母團隊。

在某些情況下,雖有合適的母團隊但尚未準備好接受子團隊時,啟動台能作為臨時歸宿容納這些例子。

而對於團隊面臨移除或重組時,如果移除或重組時並沒有明確將其子團隊納入其它組織中的母團隊,則啟動台將會作為這些子團隊的預設歸宿。

理事會必須每六個月審視「啟動台」中的子團隊成員狀況,以確保有合適的流程在協助所有子團隊成員找到新的母團隊。而與其它一級團隊相同的是,只要理事會認為「啟動台」團隊不再需要,就可以將之撤除(並且移除掉其在理事會中的代表)。撤除「啟動台」團隊的流程與其它一級團隊同理。除此之外,理事會也可以給予「啟動台」團隊自己的權責,但是此措施不屬於本案的範圍內。

廢除一級團隊

任何要移除團隊的一級指定(或其它會導致無法參與理事會)的決策,都需要除卻將被移除的一級團隊代表外所有理事會代表的共識。雖然如此,必須要邀請考慮移除的團隊代表參與關於團隊移除的理事會審議,而理事會也只能在極端特例下不顧團隊反對將之移除。

理事會不得移除審核團隊。理事會不能在沒有審核團隊的同意下改變審核團隊的權責。

候補與放棄代表權利

若有必要,例如工作時間或是情況變化,代表可提前結束任期。該團隊則須開始選任新代表。代表是一志願職位,沒有人有義務擔任該職,也不允許將擔任代表作為團隊成員之義務。然而,代表自身有義務履行其職責,或是辭去該職位。

一級團隊可決定暫時放棄他們的代表權,例如,該團隊暫時人手不足且無人自願擔任代表。然而,若該團隊沒有指定理事會代表,視同放棄積極參與整個專案的決策權。其他所有理事會程序,包括決策,不應因此缺漏而受阻。理事會仍有義務考慮來自所有專案成員的訊息與異議。然而,理事會並無義務阻擋任何決策,來特別考慮或整理未被代表的團隊之回饋。

派遣代表參加理事會是一級團隊的職責所在,無法定期參加理事會表示該團隊未盡其職責。然而,因臨時生病,休假等因素而暫時缺席,理事會代表一職並不會因此棄權。

一級團隊可指定候補代表,在其主要代表無法出席的情況下候補。候補代表將承擔主要代表的一切職責,直到主要代表回歸。當主要代表出席定期會議時,候補代表並不會參加(以避免參與人數倍增)。

如果一個團隊的代表和候補代表連續三週未參與理事會的任何程序,直到該團隊能提供能夠參與的代表之前,該團隊之代表將不再列入理事會決策法定人數要求。理事會須在此生效前通知該團隊。若一個團隊希望理事會不會在缺少該團隊意見,或無法代表該團隊提出反對的狀況下進行決策,該團隊應要確保有一名候補代表。

若有必要,一級團隊可在任期結束前替換其代表。然而,考量維持連續性的成本,團隊應避免非必要時不替換其代表。團隊的首要職責是向他們的代表和候補代表介紹在團隊中希望能持續處理的問題與立場。理事會和團隊共同負責維持理事會內部正在處理的問題之連續性,並向主要以及候補代表說明情況。

有關私人事務,理事會將酌情通知候補代表,以避免分享不必要的私人資訊。若候補代表需要出面,則理事會可說明該情況。

任期限制

理事會代表任期為一年。對於任何已被認可的代表團(來自特定一級團隊的代表團),每位代表有最多連任三次的非硬性限制。一位代表在理事會收到來自個別團隊明確指出他們無法產生另一位代表時,得超過任期限制(舉例來說,因為缺乏其他有意願的候選人,或是因為團隊成員對其他任何候選人提出反對意見)。

除此之外,對於單一代表在其它一級團隊服務的任期數、或是在單一一級團隊的非連續任期,並沒有任何硬性限制。團隊應該力求在經驗的存續與代表的替換之間取得平衡,以提供更多人這樣的經驗。7

一半的代表將在(每年的)三月底進行任命,另一半則在九月底進行任命。這避免了同時更換所有理事會代表。對於初始的理事會以及在任何時候的一級團隊組成發生改變,理事會以及一級團隊應該共同努力使任期結束日大約平均落在在三月與九月各半。然而,每個任期應該至少持續六個月(為了避免任期過短,暫時的不平衡是可被接受的)。

如果理事會及一級團隊無法就適當的任期結束日變更達成共識,代表將在三月底或九月底兩者之間被隨機分配一個任期結束日(且在至少 6 個月後)以維持平衡。

對單一公司或實體代表之限制

理事會代表不得過度來自同一家公司、法人實體(legal entity)或密切相關的法人實體,以避免出現不當或疑似不當行為。若理事會代表人數不超過五人,則不得超過一名代表有相同隸屬關係;若理事會有六名或更多代表,則不得超過兩名代表有相同隸屬關係(affiliation)。

密切相關的法人實體是指包括同一實體的分部(branch)/部門(division)/子公司(subsidiary),透過實質上所有權之權益相關聯的實體或類似實體。理事會可在特殊情況下做出判斷,以避免該決策中的利益衝突。

若理事會代表的大部分收入來自一公司或法人實體(例如來自雇主、客戶或主要贊助商),則表示該代表與其有隸屬關係。該代表須即時揭露其隸屬關係的變化。

無論因為代表更改隸屬關係、一級團隊任命新代表或理事會人數規模變化,而導致違反上述限制條件,則按照下列方式恢復之:

  • 具有相同隸屬關係的代表可先試圖在其內部解決問題,例如一名代表自願下台,其所屬團隊再任命其他人。
    • 這必須由代表而非其隸屬實體決策;隸屬實體影響該決策視同不當行為。
    • 每名代表在此類討論中皆享有平等地位;不得使用在專案或理事會的資歷等因素施壓他人。
  • 若有相同隸屬關係的代表無法達成一致,則隨機刪除其中一名。(若仍違反限制條件,剩下的代表可再次嘗試內部解決,再重複此過程。)這很有可能是次佳解;自願解決方案通常更好。
  • 雖然團隊應立即啟動選擇接班人的程序,但團隊現任代表仍可續任其剩餘任期至多三個月。
  • 現有代表應與繼任代表協調交接事宜,不過在三個月的過渡期間,該團隊須選擇哪名代表是實際代表。一個一級團隊無論如何只能有一名代表。

候選資格

以下是決定理想候選人的標準,這些標準雖類似團隊領導者或共同領導者的標準,但並不完全相同。儘管團隊領導者可能也能成為傑出的理事會代表,但擔任團隊領導者和理事會代表都需要大量時間投入,這會促使將這些角色分配給不同的人。這些標準並非硬性要求,但可用於決定誰最適合成為團隊代表。簡而言之,代表應具備以下條件:

  • 有充沛的時間與精力,以滿足理事會的需求。
  • 有興趣協助處理專案營運和治理相關事務。
  • 廣泛了解專案的需求,且不限於其所在團隊或積極貢獻之領域。
  • 對自己的團隊需求有敏銳的感知。
  • 具備代表他人的氣質和能力,超越任何私人事務。
  • 能夠代表其團隊的所有觀點,不僅僅是部分觀點,也不僅僅是團隊成員同意的觀點。

儘管有些團隊目前可能沒有足夠的候選人符合這些標準,理事會應積極培養這些技能之人才,因為這些技能不僅對理事會成員資格有幫助,也對這個專案有益。

與 Core 團隊之關係

The Leadership Council serves as the successor to the core team in all capacities. This RFC was developed with the participation and experience of the core team members, and the Council should continue seeking such input and institutional memory when possible, especially while ramping up.

External entities or processes may have references to "the Rust core team" in various capacities. The Council doesn't use the term "core team", but the Council will serve in that capacity for the purposes of any such external references.

與 Rust 基金會之關係

The Council is responsible for establishing the process for selecting Project directors. The Project directors are the mechanism by which the Rust Project's interests are reflected on the Rust Foundation board.

The Council delegates a purview to the Project directors to represent the Project's interests on the Foundation Board and to make certain decisions on Foundation-related matters. The exact boundaries of that purview are out of scope for this RFC.

理事會決策流程

領導理事會作出兩種不同類型的決策:營運決策和政策決策。並可能會根據決策的類別,給予不同的考慮。然而在預設情況下無論分類為何,理事會將對所有決策使用共識決策流程。

營運與政策決策

營運決策是理事會為實現其目標而每天作出的決策,包括在會議之外進行的常規行動(基於既定政策)。政策決策提供通用且可重複使用的模式或框架,旨在構建、指導和支援營運。特別是政策決策可以為營運決策或營運的其他方面提供部分自動化。除非在本 RFC 或其他政策中另有規定,否則理事會對所有決策預設使用共識決策流程。

本 RFC 並不會試圖精確界定哪些決策是營運決策,哪些是政策決策;實際上決策將會落於連續光譜中的其中一處。進行此項區別的目的並不是指示或限制理事會的決策程序。與此相對的,這種區別為理事會提供了指引,並澄清理事會如何隨著時間記錄、審視和完善其決策。而對於與營運或政策分類相關的任何要求或指示,任何沒有在本案或者未來政策中標示為營運或政策決策者,都將預設視為政策決策。

重複與例外

政策決策通常會以系統化的方式解決可能需要反覆進行的營運決策。理事會應該致力於認知反覆的營運決策顯現需要的政策決策或政策變更。特別是理事會應該避免讓反覆的營運決策構成事實上的政策。

除非該政策明確允許例外,否則不能通過營運決策對現有政策進行例外。避免臨時例外有助於避免「偏差常態化」

共識決策流程

理事會最初將採用一種確定同意提案的流程。然而也期待理事會在成立後不久將在其工具組中加入額外的流程。

共識意味著不能忽略任何代表的要求(因此也包括他們代表的一級團隊和子團隊的要求)。理事會聽取所有相關意見,並會為了能與所有聲浪平等合作而奠定良好基礎。

理事會採用共識決策,不會以「你同意嗎?」詢問,而是以「你反對嗎?」來詢問代表。這樣可以消除「口袋否決」,即在完整審查提案後,卻在沒有提供明確回饋的情況下否決掉。考量、回饋、偏好和其他不關鍵的回饋形式並不會阻止決策的做出,但在起草和討論的初期仍應考慮納入。反對意見,代表未滿足的要求或需求,則必須考慮和解決後才能繼續進行決策。

核准標準

共識決策流程具有以下核准標準:

  • 將提案發布在領導理事會指定的溝通空間之一(會議或特定管道)。
  • 確認至少 N-2 理事會代表(其中 N 是理事會代表的總數)已經完全審查了最終提案並給予同意。
  • 無任何理事會代表未解決的明確反對意見。
  • 提供至少 10 天的回饋時間。

核准標準提供了法定人數機制,並提供代表足夠時間查看提案。允許兩個非簽署人是對專案志願性質的認同,基於在決策速度與共識和無異議所需確認量之間取得平衡的經驗加以制定;如此設計是假定對應代表如果希望要反對,也應已有時間提出反對意見。(這是根據現在用於核准 RFC 的流程所規劃的。)

如果提議代表決定撤回他們的提案,則決策流程可以隨時結束。其他代表可以隨時接手提案以維持其活躍。

如果利益衝突導致理事會無法達到 N-2 的法定人數來作出決定,則理事會不能作出該決定,除非遵循「利益衝突」段落中關於如何在記錄衝突下繼續進行決策的流程。在這種情況下,理事會應該考慮適當的流程和政策,以避免類似衝突的重演。

變更與調整決策流程

利用公共政策流程,理事會可以為不同類型的決策建立不同的決策流程。

例如,理事會肯定會希望對一部分營運決策的子集進行快速決策,而無需等待所有代表積極回應。本 RFC 中沒有定義這樣的機制,但建議理事會將其作為首要行動之一加以制定。

在決定采用哪種決策流程來處理特定類型的決策時,理事會需在快速決策與完全一致性的信心之間取得平衡。共識決策流程包括以下範疇:

  • 一致共識決策(以犧牲快速決策為代價,提高完全一致性的信心):團隊成員必須審查並優先考慮該提案而非其他提案,任何團隊成員都可以提出阻止性反對意見。
  • 共識決策(理事會的預設方式,平衡快速決策與一致性信心):團隊成員必須審查並可以提出阻止性反對意見。
  • 一個贊成且無異議(以犧牲一致性信心為代價,優先考慮快速決策):一個團隊成員必須審查並支持,任何團隊成員都可以提出阻止性反對意見。

任何定義決策流程的政策都必須至少處理以下要素:提案可以被發布的地方、法定人數要求、所需審查數和回饋的最短時間延遲。所有決策流程的核准標準都包括無異議。

如果利益衝突導致超過三分之一的理事會無法參與決策,理事會在不遵循「利益衝突」段落中關於如何在記錄衝突下繼續進行決策的流程的情況下,無法做出該決策。(無論使用的決策流程的其他法定人數要求如何,都是如此。)在這種情況下,理事會應該考慮適當的流程和政策,以避免類似衝突的未來重演。

理事會還可以通過公共政策決策將其決策範疇的子集委派給團隊、其他治理結構或由理事會創建和任命的職位,如營運負責人、會議主持人或書記、秘書。

請注意,理事會可以委派起草提案,而不一定要委派決策該提案的核准與否。這在涉及許多團隊的權責或不屬於任何團隊權責的專案整體政策的情況下可能是必要的。在逐步建立新團隊時,這也可能有所幫助。

議程與待辦事項

理事會的議程和待辦事項是理事會追蹤和更新專案成員在整個專案中提出的問題的主要界面。

為了提高議程和待辦事項的公平性和有效性,理事會必須:

  • 使用一個工具,讓專案成員能夠向理事會提交請求並接收有關這些請求的更新。
  • 使用透明和包容的流程,為近期確定優先事項和目標。其中必須包括所有代表定期的事態了解和回饋。
  • 力求在待辦事項和議程中在長期戰略目標和短期需求之間保持平衡。
  • 保持靈活和適應性,並根據不斷變化的情況或優先事項,根據需要調整待辦事項和議程。
  • 定期審視和更新待辦事項,以確保其準確反映了理事會的目前優先事項和目標。
  • 遵循明確且一致的流程將項目從待辦事項移至議程,例如將責任委派給特定職位(例如會議主持人和書記),並在會議開始時同意議程。任何在同意流程中否決的議程項目都必須在理事會公布的會議記錄中記錄其反對意見。

解決僵局

在某些情況下,理事會可能需要緊急作出決定,並且認為在這段時間內無法構建每個人都能同意的提案。在這種情況下,若每個人都同意及時作出他們不同意的決定會比根本沒有及時作出決定的結果更好,則理事會可以使用替代決策方法來嘗試解決僵局。替代流程是非正式的,理事會成員仍然必須通過現有的決策流程重新確認他們對結果的同意。理事會成員仍然可以隨時提出反對意見。

例如,理事會可以同意進行投票,然後在投票完成後,所有理事會成員都將同意投票所得出的任何決定。理事會應致力於記錄選擇特定替代決策模型的潛在優點和缺點。

根據設計,沒有強制性的僵局解決機制。如果代表們都不同意作出決定,即使他們不喜歡該決定的結果,或者如果任何代表覺得仍有可能制定出一個能夠得到理事會同意的提案,他們可以隨時保持反對意見。

如果代表撤回反對意見,或者同意他們不完全同意的決定(無論是作為替代決策流程的結果還是其他原因),理事會應安排評估或考慮提早已安排評估活動,並應建立衡量、評估表達考量之處的方法。審查的結果旨在確定理事會是否應考慮改變其先前的決定。

回饋與評估

所有政策決策都應將評估日期作為政策的一部分。初始評估期應比後續評估期短。評估期的長度應根據情況所需進行調整。對於運作良好且需要更動不多的政策,應該延長評估期以減少在不必要的審查上花費的時間。對於近期調整或受到質疑的政策,應該縮短評估期以確保對應政策能更快地迭代至穩定狀態。理事會應為政策類別建立標準化期限,以便在確定新政策的期限時使用預設值。例如,職位的評估日期最初可以為 3 個月,接著改為 1 年,而一般政策則可以預設為最初 6 個月,接著改為 2 年。

  • 新的政策決策始終可以修改或替換現有政策。
  • 政策決策必須於具有版本歷史的形式發佈在中心位置。
  • 修改生效中的政策文件時,應包含或鏈接到與政策決策相關的上下文,而不是期望人們之後去尋找該上下文。

決策流程之透明度和監督

由領導理事會作出的決策,根據其決策的性質會需要不同等級的透明度和監督。本段落將會給予指引,提供理事會對其決策尋求監督的手段,以及怎樣的決策會在私下或公開執行。

本案將會為特定的決策分類。所有沒有被特別提到的決策都必須使用公開決策流程。理事會可以透過公開政策流程為分類方式進行演變。

由理事會作出的決策會根據其監督的可能性與必要性被分為三種分類之一:

  • 理事會可在內部做出之決策
  • 理事會須私下保密做出之決策
  • 理事會須經公開提案做出之決策

理事會可在內部做出之決策

某些類型的營運決策可以在理事會內部做出,只要理事會存有機制讓社群在決策做出後能給予回饋。

要將全新的決策加入理事會可以內部做出的決策清單,必須透過公開政策決策進行。任何會影響架構、決策者或者對理事會監督的決策都不應該加入此清單。

理事會應該要致力於避免使用重複的內部決策建立起不成文的政策,藉此來避免進行公開提案。請見重複與例外的詳細資訊。

本清單會明列所有理事會可以內部做出的決策:

  • 決定展開一個會公開進行的流程(如「我們來建立並貼出問卷調查」、「我們來撰寫此未來公開決策的提案」)。
  • 表達與溝通 Rust 專案的官方定位宣言。
  • 對另一個實體(如 Rust 基金會)直接表達與溝通 Rust 專案的定位。
  • 藉由 Rust 專案的溝通資源進行溝通(透過網誌或者所有 @ 標註)。
  • 執行關於理事會內部流程的大多數營運決策,包含理事會的協調方式、使用來溝通的平台、見面的地點與時機、用於做出並記錄決策的範本(需求由本案它處規定)。
  • 為了帶領/組織會議、紀錄與發佈記錄、從各個單位取得並整理回饋等等而指派執行官。8注意這些職位(頭銜、責任以及目前的在任者)都必須要被公開揭曉與記錄。
  • 邀請理事會代表以外的特定人士參與特定的理事會會議或者討論,又或者主持開放給更廣闊社群的會議。(特別是鼓勵理事會邀請特定決策的受影響者參與該決策進行的會議或討論。)
  • 為一個或多支團隊於其正常職權範圍內請求理事會不經公開提案做出的決策。(注意團隊可以選擇請求理事會的意見而無須請求理事會做出決策。)
  • 為團隊職權重疊或模糊的情境做出一次性的判斷(但是改變團隊職權時一定要經過公開政策決策)。
  • 任何此案或者未來理事會政策指定為營運決策的決策。

請見監督與問責機制以了解理事會決策的回饋機制。

理事會須私下保密做出之決策

有些決策必然會涉及關於個人或者其它實體的隱私細節,公開這些細節會對個人或實體和專案造成負面影響(如個人與實體的安全性,和對專案的信任)。

這項額外限制應該要被視為特例。並不會允許私下做出下一段落會提到需要公開提案的決策。但是可以允許理事會於內部做出的決策能維持其隱私性,而不需要提供公開監督完整的資訊。

理事會也可以拒絕私下做出決策,如理事會認定不屬權責內事項(並且選則轉交給另一個團隊),或者相信此事項必須被公開應對的時候。然而即便在這樣的狀況下,理事會仍然不能公開揭曉所機密交付的資訊(否則理事會就沒有辦法受到信任交付這些資訊)。此明顯特例是為了避免對安全造成即刻威脅而設立的。

私下決策不能被用來建立政策。理事會應該要致力於避免使用重複的內部決策建立起不成文的政策,藉此來避免公開提案。請見重複與例外的詳細資訊。

本清單會詳盡列出理事會可以部分或完全私下做出的決策:

  • 決定與新產業/開源事業的關係時,必須於推出前維持機密性時。
  • 討論團隊之間涉及人際間互動/衝突的個人資訊時。
  • 代表專案與第三方進行契約磋商時(如接受提供給專案的資源)。
  • 涉及專案相關的爭議政治、個人安全、或者其它可能會讓人無法安全公開發言的議題時。
  • 討論一個團隊或個人是否與為何需要協助與支持,可能會涉及個人資訊時。
  • 任何此案或者未來理事會政策指定為私下決策的項目。

理事會可以將將其它團隊的成員拉入可能會帶來私下或公開決策的私下討論,除非這麼做會導致揭露給理事會的隱私資訊會在不被允許的情況下更大幅度曝光。可能的情況下,理事會應該要試著將會受到決策影響的人或團隊納入討論。如此一來也能提供額外的監督。

有些事項可能不適合進行完全公開的揭曉,但仍適合分享給能被信任的討論圈(例如所有專案成員、團隊領導者或者會涉及/影響的單位)。理事會應該要致力於將資訊分享給最大合適的對象。

理事會可以在未知資訊是否有敏感性時決定要保留決策或者決策的某些面向。然而,隨時間過去合適的分享對象更加明確或擴展時,理事會應該要回顧其資訊分享決策。

理事會應該讓審核團隊知曉涉及人際衝突/紛爭的事項,因為該事項屬於審核團隊的職權內,同時也能得額外的監督。

理事會應該要評估決策的那些部分或其相關討論應該私下保密,並且考慮是否有將非敏感的部分公諸於眾,而不該只因其中一部分應該要維持私下保密而將整件事保密。所能公布的可能性包含討論本身的存在,或者是自身細節不具備敏感性的一般性議題。

私下保密的事項在未來不再具備敏感性時,可以被公開或者部分公開。然而有些事項可能永遠都無法被公開,代表說這些事項將不可能接受更廣泛的檢視或監督。因此,理事會在做出私下決策前必須要審慎考慮。

理事會應該要盡可能不做出私下決策。理事會應該要有合適的額外流程能鼓勵代表們共同檢視如此決策並考慮其必要性。

理事會須經公開提案做出之決策

在此分類中的決策需要理事會主動提前在決策做下前向更廣泛的 Rust 專案參與者尋求回饋。這樣的決策會透過合適的公開決策流程提案並決策,目前必須透過 RFC 流程(然而理事會未來可以採用不同的公開提案流程)。公開決策流程必須有所有代表的共識(無論是透過主動同意或無反對的形式),必須接受理事會代表的阻止性反對意見,必須有合理的時間接受公開評估與討論,並且必須提供公開回饋給理事會的明確途徑。

依循既有的 RFC 流程,公開提案必須要在決策生效前有個最低的延遲時間以接受回饋。任何代表都可以要求特定決策接受回饋的時長最多延長至總計 20 天。理事會可以做出內部營運決策來延長回饋期間至超過 20 天。接受回饋的延遲期間只有在通過決策的必要條件都滿足時才能開始計算,包含沒有任何提出的反對。如果在延遲期間有反對被提出並且解決,延遲等待期間將會再次重新計算。

領導理事會被期待能隨時間演化以應對團隊、Rust 專案以及社群需求的演變。如此演變的規模可大可小,即會需要對應的監督程度。會在本質上影響理事會形式的變更必須要被納入公開決策流程。

作為上述的例外,對於一個一級團隊(審核團隊除外)的調整或移除可以在對應一級團隊委派代表缺席的情況下由一致同意做出。

理事會被允許進行私下討論可能會最後成為公開提案或者公開揭露的內部決策,討論的敏感性使得私下討論能允許決策參與者更自由坦蕩地發言時可以如此進行。除此之外在特定例子下,無法被揭曉的隱私資訊可能會影響到一個公開決策/提案,此情況下理事會應該要致力於維持透明性與降低誤導資訊,並且避免做出所有判斷都私下進行的完全不透明決策。

注意除非有被明確指出(透過此案或者未來的公開提案),否則所有決策都隸屬於此分類,因此此清單(與其它兩個分類不同)是刻意有模糊與廣泛性:目標是在不需要明確規定的情況下提供指引怎樣的決策會隸屬於此分類。

  • 任何會修改領導理事會的決策者清單或者決策流程的決策。例如:
    • 改變這個清單(或者對此案的修改)。
    • 修改用於發表與核准理事會公開提案的流程。該提案必須使用既有的流程進行,而不能使用所提案的流程進行。
    • 增加、修改或移除會影響理事會成員資格的政策。
    • 增加、修改或移除一個或多個一級團隊。其中包含:
      • 修改一個一級團隊的權責到會使之實質上成為不同團隊的程度時。
      • 重新組織專案到頂級團隊會會隸屬到其它團隊下的程度時。
    • 增加除了一級團隊委派的代表以外的理事會代表。
    • 增加、修改或移除政策關於理事會法定人數或能制定具約束力的決策所能制定的地點時。
  • 任何政策決策,除了一次性的營運決策以外(見決策相關段落以了解關於政策決策與營運決策差異的細節)。其中包含任何會對其它專案中的部分(如其它團隊或個人)造成約束力,實質成為所有團隊普通權責例外的決策。在此舉例一些政策決策:
    • 修改或延長既存的政策,包含過去透過 RFC 決議的政策。
    • 會影響 Rust 專案軟體或者其它 Rust 專案成果的條約/授權政策。
    • 對行為準則進行的變更。
    • 會影響 Rust 專案成員與任何團隊資格的政策。
    • 對審核團隊如何審核理事會代表與領導理事會整體的變更。此類決策必須與審核團隊共同決策。
    • 代表 Rust 專案整體對其它專案或組織做出長期承諾的協議。(涉及團隊已經同意承諾的一次性承諾則不受此限)
    • 創造或者大幅修改條約架構時(如增設額外的基金會、改變與 Rust 基金會之間的關係、與其它法律實體合作時)。
    • 根據一個或多個團隊制定隸屬於這些團隊普通權責範圍內的政策決策時。(注意團隊可以請求理事會提供意見而不需要請求理事會做出決策)
    • 決定某類型的未來決策都將永遠都由理事會做出,而不委派給其它團隊時。
  • 任何本案或未來理事會政策指定為公開政策決策的決策。

利益衝突

理事會代表不應參與或影響其具有利益衝突的決策。

前在的利益衝突包含,但不限於:

  • 個人:與他們個人有關的決策
  • 財務:會對該代表造成重大的財務影響
  • 聘僱或同性質決策:涉及同公司的他人或者對該公司會相對其它公司造成顯著較多的好處/壞處的決策
  • 專業或其它相關性:會涉及該代表相關的組織的決策,例如產業/專業/標準/政府組織
  • 家庭或朋友:決策涉及對象使得無法期待該代表能公正決策時,包含用其它形式與該人相關的利益衝突(例如家庭成員的事業)

理事會代表必須及時揭曉其利益衝突並迴避會受影響的決策。理事會代表還必須每年主動向其他代表和審核團隊揭曉可能會造成潛在衝突的來源。

請注意,即使一項提案中沒有提及特定的實體,也有可能會造成利益衝突。例如,理事會代表不能利用他們的職位調整提案中的需求,使其雇主不成比例地受益。

就算一名代表的雇主或者同性質的存在偏好一項提案的大致內容,對 Rust 社群整體都偏好的提案並不會自動導致一名代表產生利益衝突,只要提案本身不偏好特定的實體。舉例來說,一項提案改善特定 Rust 元件安全性的提案,並不會因為代表們的雇主對 Rust 的安全性有偏好而產生利益衝突;然而如果一項提議會涉及特定開發者、安全專家或特定人士能預期從此提案中獲得利益時,則可能還是會造成衝突。

即便被理事會視為輕微衝突,理事會也不得無視任何有效的利益衝突。然而理事會可以評估是否存在衝突。理事會代表必須要提出潛在的衝突讓理事會能夠作出此判斷。

理事會可以會向迴避的代表請求特定資訊,而迴避的代表也可以在受到請求時提供該項資訊。

在可能且實際時,理事會應該要拆開決策以降低利益衝突的影響範圍。舉例來說,理事會可以將對特定類型硬體的取用決策(而不指定特定需求或選擇提供者)與具體購置硬體與購買地點的決策分開,使得特定利益衝突只會影響到後者的決策。

同時考慮 Rust 專案與任何專案團隊的利益時並不必然致使利益衝突。特指受團隊委派的代表本來就被期待要參與與該團隊相關的決策。

在不太可能發生的情況下,如果一項決策提案與足夠多的代表產生了利益衝突,以致其餘的代表不能滿足過去確立的法定人數要求,而決策仍然必須做出時,則可以應對的方式包含一級團隊必須為此特定決策提供備用的代表,或者(僅適用於公開決策時)理事會可以選擇繼續進行決策,同時公開記錄所有的利益衝突。(請注意,即使在記錄了衝突的情況下,繼續進行公開決策,實際上並不會消除衝突或防止對決策的影響;只會允許公眾判斷衝突是否影響了決策。完全消除衝突永遠是最好的作法。)在這種情況下,理事會應該要考慮合適的流程與政策,以避免未來再次發生類似的衝突。

確定與更改團隊權責

理事會可以在既存或新創的一級團隊(除審核團隊外)的權責之間移動領域或活動。即便特定一級團隊的權責可能會被該團隊進一步細分,理事會只會移動或調整一級團隊的權責。如果一個被細分的權責進行了移動,理事會則會與相關團隊合作,協調合適的下一步。此機制的使用時機只限於理事會認為既有團隊的權責過於廣泛,使得無法期待該團隊於現有架構下能夠完全實現其職責下使用。然而,當一個團隊只是目前缺乏資源來履行其部分職責時,則不應該如此進行。

理事會還必須負責核准一級團隊權責的擴張,並且必須在一級團隊職權的減少收到通知。此情況通常會在一個團隊自行決定他們希望擴大或減少其權責時發生。這也可能發生在部分一級團隊同意調整其之間的權責。理事會對於權責變化的認知是必要的,以確保理事會能重新指派或刻意不加以指派該項權責。

然而單獨或聯合的團隊可以在無須理事會核准之下進一步將其權責委託給子團隊。然而即便進行了委派,一級團隊仍應該對分配到的權責負起全責(換句話說,團隊有責任確保其委派能成功達成)。

理事會應該要偏好在團隊間權責移轉前與各個團隊合作找出替代方案,因為此項措施是非常重大的步驟。同時值得注意的是,這個機制的一個使用情境是將過去委託於一個不再存在功能性的團隊的權責加以轉移(例如該團隊的成員都沒有足夠時間),潛在可以於有對應時間與能力的人手到位重新建立該團隊時可以作為暫時處置。本案的此段落是刻意保留不限制理事會如何(與否)進行此諮商。

監督與問責機制

以下是諸多理事會可以確保自身與他者能負起責任的機制。

確保能追究理事會之責任

理事會必須公開確保持續達到專案整體與社群對理事會的期待。其手段包含調整理事會的政策、措施與產出結果,同時在專案與社群期待不夠現實時予以充足的教育。

要達成這件事情,除了輪替代表並採納「預設的公開性」觀點以外,理事會也應該定期(會至少每季)對他們的活動進行廣大的公開溝通,同時也要評估根據理事會的職責、期待與限制為標準去評估理事會的運作狀況。

每一年,理事會都應該要向所有願意的專案成員徵求回饋以了解理事會是否有效地完成其目標,並且在一場允許並鼓勵所有專案成員主動參加的論壇中公開討論回饋。為了達到這件事,理事會與其他專案成員要討論本案與未來改版中提到的高層級職責、期待與限制,以判斷理事會是否達成其職責與義務。

除此之外,每位代表個人都有責任注意、指出並拒絕無法依循本案、其它理事會政策或其它方面理事會責任的情境。代表應該要致力於主動迴避「責任分散效應」,即群體中會因為個別成員(有意無意地)認為其他人會負責而一同無法達成特定事項的現象。理事會也可以指派一個特定職位來負責應對並監督程序性事務,特別是負責提出程序上有秩序問題,然而其他代表依然能做這件事情。

如果任何前述的流程中得出了理事會並沒有達成其義務,則理事會必須要盡快提出計畫,協助理事會改善,以利其達成目標。其方法可能是提出修改 RFC 或相似的措施、代表的輪替或者其它重大變更。任何計畫都應該對理事會或 Rust 管理整體上,如何根據去年經驗演變有明確的措施。

確保能追究理事會代表之責任

理事會代表應該要參與他者與其對應一級團隊(其性質則在本案範圍外)的定期回饋,以反映他們作為代表實現其職責的狀況。此回饋活動的目的是要幫助代表了解自己能如何更有效幫助專案。此回饋必須要分享給所有代表、所有代表的一級團隊與審核團隊。此項回饋應該要同時詢問代表做得好與能改善的部分。

除此之外,代表也應該要隨時開放其團隊與其他代表的私下回饋,並且應該要定期省思自己在理事會中的職位與成效。

此種回饋流程的產物絕不能被公開,以確保流程的安全與公正。理事會也應該要在結果無法帶來正向改變時反映並改變回饋流程。

如果其他理事會的成員認為一名理事會代表與其餘理事會成員並沒有辦法良好合作,則他們該與對應代表進行對話,而有必要時也該向該代表的團隊對話。理事會代表也必須要納入可以促進這些對話進行的審核/調解資源。審核可以幫助解決問題,且/或能判斷是否需要為此問題行動並且是否需要擴張應對的層級。

雖然個別團隊要如何確保能追究其代表責任並不在本案的範圍內,我們鼓勵各團隊以上述機制為靈感去規劃各自的政策與措施。

確保能追究團隊之責任

各團隊要定期彼此協調與合作,並且溝通自己的需求;在正常狀況下,理事會必須尊重個別團隊的自主性。

然而,理事會也作為各團隊可以共同以團隊或專案整體來追究彼此責任的手段。理事會可以:

  • 請求一個團隊重新考慮一個並未納入其他團隊或者專案整體考慮的決策。
  • 鼓勵各團隊建立可以定期將其他團隊納入考慮的流程。
  • 確保對所有團隊權責有共同的瞭解。
  • 確保各團隊都願意並且有能力實現這些權責。
  • 建立新團隊將一個團隊的權責分為更能被應對的大小。

追究的流程不得具備懲罰性,同時此流程必須在受到影響團隊的主動合作下進行。

在極端情境下,如果團隊刻意選擇對專案整體不以良善原則行動時,則理事會有權力去改變團隊的權責、將部分權責轉移至其他團隊或直接移除整個團隊。此作為可以透過理事會常規的決策流程進行。(此項措施對審核團隊並不適用;請見下一段落關於理事會與審核團隊之間的問責機制)

審核、分歧與衝突

本段落描述了領導理事會和審核團隊在協助解決分歧和衝突方面的角色,以及這些團隊之間的互動。

分歧和衝突同樣座落在人際互動光譜上。分歧是更多關於事實和、或技術上的不一致,而衝突則是更多關於社交或關係上的合作障礙。許多互動可能同時表現出分歧和衝突的樣貌。理事會可以在分歧方面提供幫助,而衝突方面則屬於審核團隊的權責。

本案並未詳細規定一般審查政策,僅規定了與理事會互動以及理事會與審核團隊之間平衡的部分。一般審查政策超出了本案的範疇。

Rust 專案的許多工作都涉及與其他人的合作,所有人都非常關心自己的工作。人與人之間有分歧很正常,對分歧產生強烈情緒也很正常。分歧也可以成為揭示和解決問題的有力工具,理想情況下,有分歧的人可以在合作中(盡量)友善地探討這些分歧,而不升溫為人際衝突。

分歧和衝突出現的情況可能很複雜。分歧可能升溫為衝突,衝突也可能降級為分歧。如果情況中無法明確區分分歧和衝突,或者參與者對此有分歧,則應假定情況是衝突。

在發生衝突的情況下,涉及者應盡快聯繫審核團隊協助解決衝突。時間是在衝突惡化或造成更多傷害之前解決衝突的關鍵資源。

團隊間之分歧

在可能的情況下,團隊應嘗試自行解決分歧,如有需要可向理事會尋求協助。理事會可以做出判斷來解決分歧,但團隊需要保持良好的工作關係,以避免持續的分歧或升級為衝突。

團隊之間分歧的潛在解決途徑可能包括選擇先前討論過的選項,制定新選項,確定決策屬於哪個團隊的職責範疇,或者決定該決策在兩個團隊的權責範疇之外,交由理事會為這項工作找到一個新的歸屬。

涉及團隊或專案成員之衝突

涉及團隊或專案成員的衝突應盡快提交給審核團隊。理事會可以協助減少這些衝突對未決、緊急決策的影響,但審核團隊負責協助解決團隊之間或其他方面的衝突和人際問題。

個人或團隊也可以自願參與其他過程來解決衝突或人際問題,例如非約束性的外部審核。在這樣做時,個人或團隊應讓審核團隊知道情況,並尋求審核團隊對於合適的資源或方法的指導。個人或團隊不得使用會產生利益衝突的資源。

審核人代表團

審核團隊必須始終維護一個公開記錄的「緊急審核者」名單,這些緊急審核者必須通過審核團隊和理事會的內部共識決策得到批准。審核團隊和緊急審核團隊應分別由至少三名成員組成。緊急審核者必須:

  • 既不是現任審核團隊的成員,也不是領導理事會的成員。
  • 由理事會和審核團隊共同確定,受到 Rust 專案成員的廣泛信任;這通常意味著他們已經以某種方式參與專案。
  • 由理事會和審核團隊共同確定,具有從事審核工作和稽核的資格。更詳細的標準和指導方針將由審核政策確定,該政策超出了本案的範疇。
  • 願意擔任緊急審核者:願意進行審核,並在審核團隊解散或無法進行工作的情況下,願意進行臨時審核工作,直到他們能夠任命新的正式審核者。(緊急審核者無需願意長期從事審核工作。)
  • 願意熟悉審核政策和程序,達到審核團隊成員所期望的標準(包括任何相關培訓)。在可能的情況下,緊急審核者應獲得與審核團隊相同的培訓機會。

需要緊急審核者行動的情境會是高壓緊張的情況,而專案和理事會必須做好信任他們投身於這種情況的準備。選擇其他專案成員所熟悉和信任的人有助於降低這種情況下的緊張。

審核是一項容易耗竭的活動,個別審核者或審核團隊可能會希望遠離這項工作。注意,一個或多個個別審核者可以選擇辭職,此時審核團隊應該確定並引入新的審核者來填補任何空缺或不足;如果審核團隊要求緊急審核者成為正式審核者,該小組應該任命一位新的緊急審核者。辭職的個別審核者可以被選為緊急審核者。如果審核團隊整體同時無法進行工作(由理事會和緊急審核者通過內部共識決策共同確定),或選擇同時辭職,緊急審核者將成為臨時審核團隊,並必須迅速任命新的緊急審核者,並開始尋找新的正式審核者。

由於緊急審核者角色除了特殊情況以外不需要執行任何定期活動,因此被任命為該角色的人必須與審核團隊定期檢查,以確認他們仍願意擔任該角色,並避免緊急審核者突然被需要並被發現無法進行工作的情況。

審核團隊之政策與程序

審核團隊有責任制定完善的政策和程序。理事會提供監督和協助,以確保審核團隊擁有這些政策和程序,並且它們足夠完善。

理事會可以向審核團隊提供回饋,審核團隊有義務考慮收到的所有回饋。如果理事會認為審核團隊未遵循審核政策和程序,理事會可以要求緊急審核者進行稽核。然而,理事會不能否決審核決定或政策。

稽核

如果任何理事會成員認為審核決定(或一系列決定)未遵循審核團隊既定的政策和程序,他們應立即通知審核團隊。理事會和審核團隊應該相互合作,討論和理解這些問題,並努力解決它們。

本案提供的一種以保密形式檢查審核團隊行為的機制,是稽核(audit)機制。在任何情況下,如果任何理事會成員認為審核團隊的行動未遵循記錄紀錄有案的政策或程序時,則該理事會成員可以決定啟動稽核流程。(特別是,理事會成員可以以此形式回應涉及審核情形的社群成員的回報。)此項機制的進行是除了以上的互動和對話之外的額外行動;此機制不是理事會與審核團隊之間直接溝通的替代品。

在稽核中,緊急審核團隊與審核團隊合作,確定審核團隊是否遵循既定的政策和程序。這一機制必然涉及緊急審核團隊依據自己的判斷來評估審核政策、具體證據或溝通以及相應的審核行動或建議行動。然而,稽核機制並不旨在對行動本身進行懷疑;稽核機制的重點是確定審核團隊是否按照其既定的政策和程序行事。

緊急審核者還會與理事會聯繫,了解他們可能需要的任何額外背景資訊。

審核流程和稽核都需要時間,並且必須謹慎執行。然而,理事會、緊急審核者和審核團隊都應該努力在合理的時間內相互表達他們的顧慮和期望,並保持開放的溝通管道。

緊急審核者在具有利益衝突的情況下不得參與決策或稽核。在緊急審核者被公開列為緊急審核團隊成員之前,他們不得查看提交給審核團隊的私人資訊;讓與審核團隊交流的人有機會評估潛在的顧慮或利益衝突。

與理事會和緊急審核團隊的討論可能發現,審核團隊在某個特定案例中不得不在政策上作出例外,因為政策中存在意想不到的條件,或者存在有無法納入政策的背景資訊。這是一個需要緊急審核團隊對例外的理由和決定例外必要性的流程進行額外審查的預期情境,但本質上並不違反審核團隊職責。

隨著稽核流程和理事會、審核團隊討論的進行,審核團隊可能會決定修改審核政策和、或更改特定審核決定或擬議決定的結果。這完全取決於審核團隊的決定。

緊急審核團隊必須將稽核結果報告給審核團隊和理事會以供他們審查。這不得包括任何可能直接或間接透露私人資訊的細節。此報告與和審核團隊的討論相輔相成,最終旨在解決理事會的顧慮。

最終問責機制

Rust 專案的領導理事會和審核團隊在專案內各自擁有相當大的權力。本案提供了許多工具來解決衝突。本段落概述了這些團隊相互追究責任的最終問責機制。本段落寫成之時期許將永遠不會被用到,希望所有團隊會盡一切可能在達到此情境之前解決衝突。

如果理事會認為審核團隊存在系統性問題(無論是基於緊急審核團隊的稽核報告還是其他原因),並且理事會和審核團隊無法自願達成協議來解決問題,那麼作為最後手段,理事會(經一致決定)可以同時解散自己和審核團隊。頂級團隊必須為理事會指派新的代表,緊急審核團隊成為新的臨時審核團隊。

相反地,如果審核團隊認為理事會存在系統性問題,並且理事會和審核團隊無法自願達成協議來解決問題,那麼作為最後手段,審核團隊(經一致決定)可以同時解散自己和理事會。只有在審核團隊成員至少有三人的情況下才能實施這一過程。最高層次的團隊必須為理事會指派新的代表,緊急審核團隊成為新的臨時審核團隊。

審核團隊在理事會的代表在決定解散理事會和審核團隊時必須回避,以避免利益衝突,但該代表仍需辭去職務。

被解除職務的代表和審核者在至少一年內不得在理事會或審核團隊中任職。

預設情況下,新理事會和臨時審核團隊將負責清晰地溝通過渡期。

這個機制是絕對的最後手段。最起碼此手段幾乎肯定不會產生最佳的結果。如果情勢已經升溫到造成此結果,必須要有很多事情都已經陷入非常糟糕的情境了,而那些負責清理善後工作的人應該努力防止這種情況再次發生。(無論是審核團隊還是理事會)表示情勢可能升溫到這個程度時,都應該被認為是一個強烈的信號,讓各方一起討論找到一個「總之不要用上那招」的方法來避免這種情境。

涉及專案成員之審核措施

在進行審核工作過程中,審核團隊不僅需要對 Rust 社群成員採取行動,還需要對 Rust 專案成員採取行動。這些行動可能涵蓋從對話到從專案中移除之間的所有可能升溫措施階級。這使得審核團隊處於需要具備權力和信任的地位。此案旨在為審核團隊及理事會提供適當的問責和相互檢查手段。

如果審核團隊計劃對任何 Rust 專案成員實施外部可見的制裁(任何可能引起顯著缺席的行動,例如從職務中移除或從某專案空間中排除參與超過一週),則任何一方都可以通過與理事會或緊急審核者聯繫來要求進行稽核,並且該稽核將自動獲准。

在此案批准後的第一年內,即使沒有請求也會自動執行稽核,以確保該流程正常運行。在那之後,理事會和審核團隊將共同審查並決定是否更新此條款。

當審核團隊向專案成員發出警告,或者向專案成員發出有關審核行動的通知時,該消息將提及要求稽核的選項。

應盡快將有關專案成員的衝突通知給審核團隊知曉。

涉及理事會代表之衝突

涉及理事會代表或替補代表的衝突遵循與涉及專案成員的衝突相同的流程。審核團隊具有與專案其他成員相同的審核代表或替補代表的能力,包括對任何外部可見制裁所需的稽核。此項行為仍然受到與審核團隊其他決策相同的問責機制的約束。

除了既有的審核行動範圍之外,審核團隊還可以在接近最後手段的情況下,作為使用升溫措施階級中較輕微的手段,從專案中移除代表或替補代表。這些行動並不是特別針對理事會的行動,並且適用於其他 Rust 團隊。

  • 審核團隊可以決定將一名代表從理事會中移除。該代表所代表的頂級團隊應立即委派一位新代表來完成剩餘的任期。
  • 審核團隊可以決定阻止一名專案成員成為理事會代表。
  • 審核團隊和理事會(排除受影響方)可以共同決定(作為私下保密共識決策)限制代表在理事會中的參與。 (在這種情況下,即便有代表存在利益衝突,他們也不會被排除在外,因為整個理事會都必須合作使制裁生效。如果利益衝突因此阻止了執行這些部分制裁,審核團隊始終可以選擇全面制裁,例如將之移除。)

所有這些行動都將觸發需要進行的稽核。理事會還必須被告知涉及代表或替補代表這樣的審核行動,或直接阻止人們成為代表的行動。

涉及審核團隊成員之衝突

涉及審核團隊成員的衝突將由餘下的審核團隊成員(扣除任何有利益衝突的成員)和緊急審核團隊共同處理,以提供額外的監督。審核團隊或緊急審核團隊的任何成員如果認為審核團隊內存在更多系統性問題,應與理事會磋商。緊急審核者必須稽核此決策並向理事會和審核團隊提供稽核報告。

本案之批准

自 2021 年 11 月以來,下列成員實質上已在專案擔任中領導角色:所有 Core 團隊成員、所有審核團隊成員、Rust Foundation 理事會(board)上的所有專案代表以及「一級」團隊的負責人:

  • 編譯器
  • Crates.io
  • 開發工具
  • 基礎架構
  • 語言
  • 函式庫
  • 審核(已包含在上方)
  • 發佈

此 RFC 將以標準 RFC 程序審批,由前述實質上的領導層成員來批准。這些成員還應代表專案中其他成員提出異議,更具體來說,團隊負責人應徵求他的團隊和子團隊的回饋。

附註

1

Unlike in some other Open Source projects, the Rust Project's "core team" does not refer to a group that decides the technical direction of the Project. As explained in more detail elsewhere in the RFC, the Rust Project distributes decision-making to many different teams who have responsibility for their specific purview. For example, the compiler team is in charge of the Rust compiler, the language team is in charge of language evolution, etc. This is part of why this RFC discontinues use of the term "core team".

2

The term 'authority' here refers to the powers and responsibilities the Council has to ensure the success of the Rust Project. This RFC lays out the limits of these powers, so that the Council will delegate the authority it has to teams responsible for the concerns of the Project. These concerns may include - but are not limited to - product vision, day-to-day procedures, engineering decisions, mentoring, and marketing.

3

Throughout this document, "teams" includes subteams, working groups, project groups, initiatives, and all other forms of official collaboration structures within the Project. "Subteams" includes all forms of collaboration structures that report up through a team.

4

Subteams or individuals that fall under multiple top-level teams should not get disproportionate representation by having multiple representatives speaking for them on the Council. Whenever a "diamond" structure like this exists anywhere in the organization, the teams involved in that structure should strive to avoid ambiguity or diffusion of responsibility, and ensure people and teams know what paths they should use to raise issues and provide feedback.

6

The Council consists only of the representatives provided to it by top-level teams, and cannot appoint new ad hoc members to itself. However, if the Council identifies a gap in the project, it can create a new top-level team. In particular, the Council can bootstrap the creation of a team to address a problem for which the Project doesn't currently have coordinated/organized expertise and for which the Council doesn't know the right solution structure to charter a team solving it. In that case, the Council could bring together a team whose purview is to explore the solution-space for that problem, determine the right solution, and to return to the Council with a proposal and charter. That team would then provide a representative to the Council, who can work with the Council on aspects of that problem and solution.

5

This also effectively constrains the number of Council representatives to the same range. Note that this constraint is independently important.

7

最終整體來說,作為一位理事會代表是為了服務個別的團隊以及 Rust 計畫。儘管此 RFC 的作者希望這個職位能夠滿足並且吸引任何足以勝任這份工作的人,我們仍然希望它不被視為是用來爭奪地位的職位。

8

The Council is not required to assign such roles exclusively to Council representatives; the Council may appoint any willing Project member. Such roles do not constitute membership in the Council for purposes such as decision-making.

Rationale and alternatives

The design space for governance is quite large. This section only attempts to address the largest and most consequential alternatives to the design decisions presented in this RFC. This section presents each such alternative along with justifications for why they were not selected.

Broader governance changes in this RFC

We considered doing more in this RFC to set up initial governance structures and improve existing governance structures. In particular, we considered changes to the existing set of top-level teams.

However, we felt strongly that anything that could be deferred to the Council should be, and that this RFC should focus on defining and delimiting the Council itself and its interactions with the rest of the Project. We felt it would go beyond the mandate of the transitional leadership structure to do much more than just architecting long-term leadership.

We also felt that further incremental evolutions would become much easier with the structures proposed by this RFC in place.

We recognize that changes to the set of top-level teams will prove especially difficult. However, we felt that the interim leadership group (including top-level team leads) would have that problem in common with the Council. Furthermore, we found that many members and leads of top-level teams were if anything enthusiastic about potential systematic improvements in this area, rather than resistant to them, even when such changes involved their own teams.

Apart from that, developing and building consensus on this RFC already represented a massive time investment by many people, and making it larger would make it take even longer.

Alternative Council structures and non-representative Council members

As an alternative to Council representatives exclusively being the representatives of top-level teams, we extensively considered other structures, whether in addition or in place of that. For instance, the Council could appoint additional members, or appoint successors, or some or all Council representatives could be elected by the Project. Such approaches could potentially make it easier to represent aspects or constituencies of the Project not yet represented by existing top-level teams, before even the nascent structures of those teams started to take shape.

Specific variants we decided not to pursue:

Non-representative Council structures

Alternative structures in which Council members are not representatives of top-level teams would have various drawbacks:

  • Any structure that does not guarantee each team a representative would provide less comprehensive and balanced representation for existing teams.
  • A structure not based on team-appointed representatives would make it harder to change representatives quickly and easily in a pinch, such as in response to changes in personal circumstances, or changes in a representative's affiliations that cause a violation of the limits placed on shared affiliations.
  • Some variants of this (such as Council-appointed additional members or Council-appointed successors) would steer the Council towards a more self-perpetuating nature that we wanted to avoid.

Ultimately, we addressed part of this issue by instead allowing the Council to easily create provisional teams (so as to introduce additional representatives on the Council), and then made room for the Council to further evolve its structure in the future by consent.

Elections

Any structure involving elections would raise additional problems:

  • Accurately determining the electorate: who precisely qualifies as being "part of the Rust Project"?
    • Many people have intuitive ideas about this, and such intuitions don't currently cause problems because we don't tie much of substance to that status. However, such intuitive definitions cause serious issues if membership in the Project determines eligibility to vote.
  • The usual problems of popularity contests: not all folks doing organizational/coordinative work are especially visible/glamorous/popular, and those doing visible/glamorous/popular work may serve the Project better doing that work rather than reallocating their time towards organizational/coordinative work.
  • Elections motivate some form of campaigning.
  • A robust election system would introduce more process complexity, both directly for the voting process, indirectly by making it harder to rotate/replace candidates in a pinch or supply alternates/backups.
  • Elections would introduce more difficult challenges when needing to change representatives quickly and easily in a pinch, such as in response to changes in personal circumstances, or changes in affiliation that run into the limits upon shared affiliations. The voters will have chosen candidates, and it's harder to go back to the electorate for new candidates, so there would have to be (for example) careful rules for selecting backup candidates based on the next lower number of votes.
  • Elections, no matter what voting system they use, inherently ignore the consent of many constituents.
  • Simpler election structures would not guarantee teams a representative, and would thus provide less comprehensive and balanced representation for existing teams. Providing more comprehensive/proportional representation of teams would add even more complexity to the election system.
    • In particular, if the people in the project fall into teams in a vaguely Pareto-style structure (a small number of teams contain a large number of people), a simple election structure may result in many teams having no representation.

We felt that we could better improve people's routes to be heard and taken into account by ensuring all governance structures and all Project members are connected through parent teams, and thus that every member of the Project has at least one representative on the Council.

Referendums

We considered introducing a full-fledged referendum system, by which proposals could be introduced, supported, and voted on by the Project as a whole. This would sidestep issues of ensuring proposals get considered and added to the Council's agenda, and would make it easier to make substantial changes not aligned with the existing Council (for better or for worse); it would also serve as an addition check and balance on the Council.

However:

  • This would have all the problems mentioned above about determining constituency in the Project.
  • This would also be a complex new structure introduced entirely in this RFC (rather than later by the Council).
    • This mechanism and its eligibility and corner cases would need to be very precisely specified, as it would often be invoked in situations with high tension and a need for a timely and authoritative decision.
  • Voting mechanisms, no matter what voting system they use, inherently ignore the consent of many constituents.
    • Voting mechanisms trend towards picking winners and losers, rather than consensus-seeking and finding ways to meet everyone's needs.
  • If such a mechanism were trivial to initiate, it could become a dysfunctional pseudo-"communication" mechanism in its own right, substituting for healthier communication and more consent-driven actions. It would, effectively, escalate problems into public dirty laundry, making it harder to resolve smaller problems. In addition, reporting on such events can generate unwarranted news like "Rust considers X" even if X has no meaningful support.
  • If such a mechanism were not trivial to initiate, the type of grassroots organizing required to successfully raise and pass such a referendum would produce better effects by working through teams, when the Project is well-aligned.
  • Conversely, if the Project has substantial issues aligning with its leadership, making individual decisions doesn't solve the underlying problem with Project health.

We chose to instead provide extensive checks on the Council itself, and mechanisms to ensure feedback and alignment between the Council and the Project, as well as a last-resort mechanism, rather than providing an ongoing mechanism to make or override individual Project-wide decisions.

Alternative checks and balances between the Leadership Council and the Project

We considered many structures for additional checks and balances between the Leadership Council and the Project:

  • We considered "vote of no confidence" mechanisms, but these would have many of the same problems as referendums, including determining the electorate, being either too difficult or too easy to initiate, and tending towards escalation rather than resolution.
  • We considered arrangements in which members of teams could directly raise objections to Council RFCs. However, this added complexity for something that the consent decision-making mechanism should make redundant.
  • We considered more formal feedback systems that could provide checks on individual Council decisions. However, any such mechanisms would also make it difficult to make timely decisions, and the blocking mechanisms would cause problems if they were either too easy or too difficult to initiate.

Alternative checks and balances between the Leadership Council and the moderation team

We went through substantial tuning on the checks and balances between the Leadership Council and the moderation team:

  • We considered making audits not automatically granted, and instead having the Council decide whether to grant an audit request. However, this would raise fairness questions for how the Council decides when to grant an audit based on limited information, as well as motivating procedural delays to give time for such an evaluation. We also felt that automatic audits (at least initially) would provide an opportunity to thoroughly test and evaluate the audit process.
  • We also considered structures using separate auditors rather than using the "contingent moderators" as auditors, but this raised severe trust issues with sharing private moderation information with those auditors.

Launching pad alternatives

We considered other alternate structures apart from the "launching pad", for handling existing teams that aren't attached to the rest of the team structure. For instance, we considered attaching such teams directly to the Council; however, this would have required special-case handling for representation that would start to look a lot like the launching pad, but with more coordination work attached to the Council.

We also considered options in which we didn't connect those teams, and permitted "disconnected" working groups and similar. This would require less transition, but would leave many Project members unrepresented and disenfranchised.

We felt that we could best improve people's routes to be heard and taken into account by ensuring all governance structures and all Project members are connected through parent teams.

We considered giving additional purviews to the launching pad, such as contributing to team organization and structure, best practices, or processes. However, the launching pad is already the one exception in which this RFC creates a new team, and we already have concerns about successfully staffing that team; we don't want to add further complexity beyond that in this RFC. The Council has the option of changing the launching pad's purview in the future.

We considered the name "landing pad" (a place for unattached teams to land) instead of "launching pad", but we felt that "launching pad" better conveyed the idea of incubating teams and helping them thrive rather than just serving as a holding area.

Double-linking

We considered adopting a "double-linking" structure between the Council and top-level teams, in which teams have two representatives on the Council, one more responsible for connecting team-to-Council and the other more responsible for connecting Council-to-team. Such redundancy could provide a firmer connection between the Council and teams, making it much less likely that concerns from teams would fail to propagate to the Council and vice versa. However:

  • Such a structure would require either an unmanageable number of Council members or far fewer than our current number of top-level teams (and we did not want to change the number of top-level teams in this RFC, or limit the number of top-level teams that strongly).
  • This would require substantial changes to the structures of top-level teams themselves to permit such linking, and such structural changes would go beyond the remit of this RFC.
  • Some models of double-linking would have one of the representatives determined by the team and the other by the Council; such a model would add complexity to the membership of the Council, such that members were not exclusively the representatives of top-level teams, which would have many of the downsides of such variations mentioned above, notably giving the Council a more self-perpetuating nature.

Recommendations for initial work of the Council

In the course of developing this RFC, and thinking extensively about the structure and operation of the Council, the interim leadership team also identified many other tasks that fell outside the scope of this RFC, and explicitly decided to defer those tasks to the new Council. This section documents those tasks, as a suggested starting point for bootstrapping the work of the Council. None of these are binding suggestions, and the Council can freely set and prioritize its own agenda; this section serves as a public, transparent handoff of knowledge and proposals to the Leadership Council.

Some of these tasks represent meta-level decisions about the processes of the Council, and we chose not to make those decisions in this RFC to avoid enshrining a particular structure rather than deferring to those who will be working regularly with that structure. The remaining tasks represent a partial todo list of long-standing tasks that fall within the Council's purview, insofar as they have fallen through the gaps between team purviews. Some of these tasks should be delegated by the Council rather than worked on directly by the Council. The inclusion of a task in this list doesn't change what type of decision-making process is required for it; some of these may be Council-internal operational decisions or private operational decisions, while others will require a public policy process.

These are in no particular order, other than that meta-level decisions about processes for making decisions will need to happen before decisions relying on those processes.

Meta-level decisions about processes and policies

  • Determining where, when, and how frequently the Council meets.
  • Establishing processes for where the Council makes decisions, both synchronously (in meetings) and asynchronously.
  • Writing and agreeing on templates for decisions, that help guide the Council to remember and follow process steps.
  • Establishing specific processes around Council transparency, including records of decisions, minutes of meetings, the locations where these get published, and similar.
  • Establishing a process for appointing the "Project directors" to the Rust Foundation board in a timely fashion. The Council will need to make such appointments soon after formation, and will also need to help ensure continuity across the transition.
  • Establishing processes and conventions for the Council's regular review of its policy decisions. In particular, establishing expectations for the frequency of such reviews, with mechanisms to adjust those downwards when representatives express concern, or upwards after previous successful reviews.
  • Selecting tools and establishing processes for tracking the Council's backlog/todo list, making as much of that list as possible public for transparency, and having a well-defined mechanism for Project members or teams to ask the Council to address something (either publicly or privately).
  • Defining and documenting processes for external requests to the Council from outside the Project, ensuring they get routed appropriately, and taking steps where possible to ensure they can be directly routed to appropriate teams (potentially including new teams) in the future.
  • Bootstrapping the new "Launching Pad" team and ensuring it has enough structure to operate.
  • Organizing teams within Rust, and ensuring all teams and other governance structures are "attached" to appropriate places in the structure. (This includes working with teams to find appropriate homes, and ensuring such changes are ultimately reflected in the team metadata repository.)
  • Establishing and agreeing on processes for faster decision-making for simple one-off operational matters, such as responding to emails reaching out to the Project.
  • Ensuring the policy decision process (RFC process) is well-documented and linked from the Council documentation, so people know how Council public proposals happen.
  • Develop handoff procedures for handling transitions to new Council representatives, or to alternates.

Other tasks/gaps potentially within the Council's purview

  • Checking in with teams and individuals across the Project, seeing what's going well and what needs help, and adding to the Council's todo list.
    • Checking for priority items that need urgent help from the Council.
    • Checking in with members of the former core team to identify items from their past todo lists and other open issues they're aware of, to add to the Council's todo list, and subsequently to either work on or delegate or otherwise disposition.
    • Checking in with the moderation team, to ensure they have sufficient support and resources to ensure growth and sustainability. Collaborating with the moderation team as they develop and codify their policies and procedures to better handle a broader range of situations across the Project.
    • Helping to develop plans to support understaffed or otherwise unsustainable teams.
  • Work with the infra team to develop a transition plan for privileges traditionally maintained by core (such as root privileges / logged-use break-glass credentials). Coordinate appropriate policies with infra.
  • Working with the Launching Pad team to help transition teams out of it into appropriate places in the organization.
  • Ensuring that top-level teams have well-documented purviews, starting to identify gaps between those purviews, and working with teams to determine when those gaps should fall to specific existing teams or become the purview of new teams.
  • Establishing policies to enable delegation of communication/PR tasks that have traditionally fallen to top-level leadership, and then making appropriate delegations of such work, potentially including the creation of teams.
  • Working with teams to establish coordination channels for team roadmaps, and developing processes to support cohesion across those roadmaps.
  • Making concrete plans to improve Rust Project diversity, including working with individual teams on how to better support diversity initiatives, as well as addressing gaps for which no individual team currently has responsibility.
  • Working with teams on processes for receiving feedback from subteams, particularly on proposed Council representatives. Particular attention should be paid to:
    • Ensure feedback is processed early, often, fairly, and consistently such that subteam members feel heard and Council members are given opportunity to address feedback and improve.
    • Help detect and address bias in Council representative selection, including status-quo bias towards existing Rust leaders or people similar to them.
  • Documenting and improving processes for interaction with the Rust Foundation, and considering organizational improvements to provide further ongoing support for those interactions (such as how and where Project directors fit into the organizational structure and how they interface with the Council regularly).
    • In particular, establishing the purview of the Project directors along with policies and procedures for ensuring proper representation of Project interests on the Foundation board.
  • Establishing proper procedures, and potentially teams, for handling issues which may have legal implications on the Project or Project participants.
  • Ensuring that people and teams within the Rust Project have access to appropriate training and resources for the positions they find themselves in, beyond the skills required for their direct purview. Foster a culture of team membership that values such skills and help teams find resources or training to bolster such skills. Such skills and training include, among many others:
    • General leadership and coordination skills, within a team and across a community
    • Transparent and legible reasoning skills, recognizing and documenting underlying values, crux-finding, and collaborative disagreement
    • Conflict resolution and de-escalation
    • Project management and planning
    • Communications between individuals, teams, and projects
    • Public communications
  • Help teams evaluate and consider replicating useful aspects of the Council's structure and processes within other teams across the Project (particularly top-level teams), such as:
    • Appropriate structures to help subteams collaborate and coordinate amongst themselves and with top-level teams
    • Structures for decision-making, including policies allowing for some types of decisions to be made more quickly, where appropriate
    • Transparency, privacy, and documentation of decisions
    • Policies for handling conflicts of interest among team members
    • Policies on the number of team members sharing a common affiliation
  • Ensure Project and Project member health by understanding and working against common work patterns where select "heroes" assume an outsized and unreasonable share of the maintenance burden by:
    • taking on large amounts of essential work that they do not really want to do because no one else volunteers
    • taking on so much work (either voluntarily or out of seeming necessity) that they are prone to burnout
    • taking on work no one else has the ability to do and for which the member's absence would lead to potential crises in the Project
  • Evaluating improvements to the RFC decision process, such as tracking and supporting multiple potential outcomes and changes in people's preferences without restarting decisions, and providing lighter-weight mechanisms for reversible decisions.

Motivation

The Rust Project is composed of hundreds of globally distributed individuals each of whom have very different motivations for working on Rust. Rust's open culture allows for these individuals to collaborate in a productive manner where complex technical and organizational decisions are made through consensus building and stakeholder feedback.

Rust's model for project management and decision-making delegates most decisions to the appropriate team. Teams are ultimately accountable for their purview. While teams' decision-making processes do not strictly need to be consensus based, stakeholder feedback is repeatedly solicited to ensure a plethora of opinions on each matter are considered and factored in.

However, at times this leads to issues. These issues can be summarized by the following questions which do not have clear answers in Rust's current governance model:

  • What happens when it is unclear which teams' purviews a certain decision falls under?
  • Who is in charge of important but non-urgent work that is not clearly owned by an existing team?
  • Who is accountable when that work does not happen and organizational debt accrues?
  • How are teams in the Project held accountable to each other and to the wider Project?

Examples of the type of work in question include the following. Please note that this list is far from exhaustive and is merely meant to give an impression of the type of work in question:

  • Helping identify gaps where existing teams are struggling to accomplish work or grow to meet challenges.
  • Establishing large structural changes such as the Rust Foundation or new top-level teams.
  • Project self-reflection. What aspects of Project operations are less than ideal? What do we do to change course?
  • Project-wide community management. While individual teams can ensure that their teams are welcoming places, how do we ensure that the Project as a whole is?
  • Policy work, for policies that have ramifications across the Project or even legal ramifications.
  • Ensuring teams coordinate their work so that the Rust Project produces results greater than the sum of the output of the individual teams.

While the current system does at times lead to positive outcomes in the above scenarios, it often does not. Some examples of failure mode categories include:

  • No one is accountable for a decision and so the decision goes unmade, leaving it undefined. This requires solutions to repeatedly be developed either "off the cuff" or from first principles. This requires enormous amounts of energy and often leads to work not being done well or at all. In some cases it can even lead to burning out Project participants.
  • Much Project work that is non-urgent often does not get done. This can lead to processes and procedures that are done not because they are the best way to handle a situation but simply because they are the easiest. This can lead to outcomes that are unfair or even actively harmful to individuals. In general, working this way leads to a culture of "putting out fires" instead of actively fostering improvements.
  • The solutions to many of the issues facing the Rust Project require coordinated action across many different teams. Finding solutions for these issues requires investment at the organizational level, and it is often very difficult for individuals to coordinate and implement such structural investment.
  • Still, such Project work is often taken up by motivated individuals who then lack structural support for accomplishing goals leading to frustration and at times conflict and burnout.

Historically, the core team delegated authority to "top-level" teams who have further delegated authority to subteams or other governance structures. However, since the work outlined above is often Project-wide and outside the purview of individual teams, delegation was sometimes difficult. As a result, the core team assumed the following two responsibilities:

  • Identifying, prioritizing, and advertising that certain important work needs to get done and does not fall under the purview of an existing team
  • Attempting to do that work

Through experience by the core team, it has become clear that both the identification of problems and the actual work itself is far too much for a single team. This has led to less than ideal results and in some cases, burnout. While a small amount of work requires urgent and immediate action, the vast majority of work would benefit from being tracked and handled by dedicated governance structures.

Non-goals of this RFC

The following are non-goals of this RFC. These may be met in future RFCs but are explicitly not part of this RFC.

Non-goal #1: Laying out the complete policies and procedures of the Council. While the RFC lays out and bounds the structure of the Council, the Council's full policies and procedures will be created by the Council itself. It is also expected that the Council will change and adapt to meet the needs of the Rust Project as it evolves.

Non-goal #2: Addressing all governance and potential governance concerns. One of the Council's responsibilities will be to identify and reflect on the issues present in governance, but we see the formation of the Council as part of a continuous process of improving Rust's leadership and how it meets the needs of the Project.

Non-goal #3: Forming additional teams. The focus of this RFC is to form the Council and does not include the creation of additional teams, subteams, or groups of any kind.

We recognize the importance of having additional teams, but see this as outside of the scope of this RFC. Instead, it will be the responsibility of the Council to investigate and understand such needs and then create additional teams to ultimately handle these issues.

This has one exception, other than the Council itself: the "launching pad" top-level team, which provides a temporary grouping of teams not yet attached to any existing top-level team either directly or indirectly.

Non-goal #4: Altering the charters or purviews of existing teams. While this RFC does discuss membership in the Council, it does not extend beyond this to update the charter or purview of any existing team. Existing teams continue to follow their existing charters and purviews.

This has two exceptions:

  • the core team: As part of this RFC, all of the capabilities and responsibilities of the core team move to the Council and are then clarified, modified, and constrained by the rest of this RFC.
  • the moderation team: As this RFC covers topics like conflict resolution and Council oversight, it does define additional capabilities for the moderation team, as well as additional checks and balances providing bidirectional oversight between the moderation team and the Council.

Non-goal #5: Establishing completely immutable properties of the Council. Any aspect established in this RFC can be modified in the future, via the public policy decision-making process, with oversight provided by that process. This RFC lays out policies for making such changes, and the processes of changing such policies must follow the existing policies.

Contributing

我們歡迎任何協助,非常感謝你願意一起支援這個社群!

授權條款

此專案的授權條款和其他 Rust 專案一樣都是 MIT/Apache2。你可以在 LICENSE-* 檔案中閱讀條款全文

行為準則

Rust 專案有一個 行為準則會囊括其下專案,這當然包含此專案。請遵守這些準則!

翻譯流程

我們的 open pull requests 接受任何翻譯或修正文章的 PR。在想開始翻譯一篇 RFC 前請先開一個 issue,如果你有希望被翻譯的也一樣歡迎開。翻譯完文章後請在每一篇文底加上以下以下資訊:

translators: [Firstname Lastname <[email protected]>]
commit: [The commit link this page based on](https://github.com/rust-lang/rfcs/...)
updated: YYYY-MMM-DD