不安全代码(Unsafe Code)

不要有人认为我软弱无能,温良恭顺; 我恰好是另外一种女人: 我对仇人很强暴,对朋友却很温和, 要像我这样的为人才算光荣. —欧里庇得斯, <美狄亚>

原文

Let no one think of me that I am humble or weak or passive; Let them understand I am of a different kind: dangerous to my enemies, loyal to my friends. To such a life glory belongs. —Euripides, Medea

系统编程的秘密乐趣在于,在每一种安全语言和精心设计的抽象之下,都是一种旋转的漩涡,这种漩涡是一种非常不安全的机器语言和小巧的混乱.你也可以在Rust写这个.

我们在本书中提到的语言通过类型,生命周期,边界检查等确保您的程序完全自动地没有内存错误和数据竞争.但这种自动推理有其局限性;有许多有价值的技术,Rust无法识别它们是安全的.

不安全代码(Unsafe code) 让你告诉Rust,”在这种情况下,请相信我(In this case, just trust me).”通过标记一个块或函数为不安全,你获得了调用标准库中unsafe函数,解引用不安全指针和调用其他语言(像C和C++)的函数的能力,以及其他权力.Rust的所有常规安全检查仍然适用:类型检查,生命周期检查和索引的边界检查都会正常发生.不安全代码只能启用一小组附加功能.

这种跨越安全Rust边界的能力使得在Rust中实现Rust的许多最基本功能成为可能,就像在C和C++系统中一样.不安全代码允许Vec类型有效地管理其缓冲区;std::io模块与操作系统通信;和std::threadstd::sync模块提供并发原语.

本章介绍了使用不安全功能的基本要点:

  • Rust的unsafe块在普通的,安全的Rust代码和使用不安全功能的代码之间建立了界限.

  • 你可以将函数标记为unsafe,提醒调用者他们必须遵守的额外合同,以避免未定义行为.

  • 原始指针及其方法允许无限制地访问内存,并允许你构建数据结构,非不安全代码情况下,Rust的类型系统将禁止的数据结构.

  • 理解未定义行为的定义将有助于你理解为什么它的后果远比仅仅获得不正确的结果更严重.

  • Rust的外部函数接口允许你使用其他语言编写的库.

  • 类似于unsafe函数的不安全traits强加了一个合同,每个实现(而不是每个调用者)必须遵循.

什么不安全?(Unsafe from What?)

在本书的开头,我们展示了一个以令人惊讶的方式崩溃的C程序,因为它没有遵循C标准规定的规则之一.你可以在Rust中执行相同的操作:

  1. $ cat crash.rs
  2. fn main() {
  3. let mut a: usize = 0;
  4. let ptr = &mut a as *mut usize;
  5. unsafe {
  6. *ptr.offset(3) = 0x7ffff72f484c;
  7. }
  8. }
  9. $ cargo build
  10. Compiling unsafe-samples v0.1.0
  11. Finished debug [unoptimized + debuginfo] target(s) in 0.44 secs
  12. $ ../../target/debug/crash
  13. crash: Error: .netrc file is readable by others.
  14. crash: Remove password or make file unreadable by others.
  15. Segmentation fault (core dumped)
  16. $

该程序借用对局部变量a的可变引用,将其转换为*mut usize类型的原始指针,然后使用offset方法在内存中产生一个指向三个字的指针.这恰好是存储main的返回地址的地方.程序用常量覆盖返回地址,以便从main返回以令人惊讶的方式运行.使这次崩溃成为可能的原因是程序错误地使用了不安全的功能—在这种情况下,能够解引用原始指针.

不安全的功能是强制签订 合同(contract,或者说契约) :Rust不能自动强制执行的规则.但你必须遵循这些规则以避免 未定义行为(undefined behavior) .

合同超出了通常的类型检查和生命周期检查,强加了针对该不安全功能的进一步规则.通常,Rust本身根本不知道合同;它只是在功能的文档中解释过.例如,原始指针类型有一个合约,禁止你解引用已超出其原始引用的对象结尾的指针.在这个例子中,表达式*ptr.offset(3) = ...打破了这个合约.但是,正如脚本所示,Rust在没有投诉的情况下编译程序:它的安全检查没有检测到这种违规行为.当你使用不安全的功能时,作为程序员,你有责任检查你的代码是否符合其合同.

很多功能都有你应该遵循的规则才能正确使用它们,但是这些规则不是我们所说的合同,除非可能的后果包括未定义行为.未定义行为是Rust坚定地假设你的代码永远不会展示的行为.例如,Rust假定你不会使用其他内容覆盖函数调用的返回地址.通过Rust通常的安全检查并遵守其使用的不安全功能的合同的代码不可能做到这一点.由于程序违反了原始指针契约,因此它的行为是未定义的,并且它会脱轨.

如果你的代码表现出未定义行为,那么你已经破坏了与Rust的交易的一半,Rust拒绝预测后果.从系统库的深处挖掘不相关的错误消息并导致崩溃是一个可能的后果;将控制权交给攻击者是另一回事.效果可能会有所不同,从Rust的一个版本到下一个版本,没有任何警告.但是,有时,未定义行为没有明显的后果.例如,如果main函数永远不会返回(可能它调用std::process::exit来提前终止程序),那么损坏的返回地址可能无关紧要.

你只能在unsafe块或unsafe函数中使用不安全的功能;我们将在后面的章节中解释.这使得在不知情的情况下更难使用不安全的功能:通过强制你编写unsafe块或函数,Rust确保你已确认你的代码可能有其他规则要遵循.

不安全块(Unsafe Blocks)

一个unsafe块看起来就像一个前面是unsafe关键字的普通的Rust块,,区别在于你可以在块中使用不安全的功能:

  1. unsafe {
  2. String::from_utf8_unchecked(ascii)
  3. }

如果没有块前面的unsafe关键字,Rust会反对使用from_utf8_unchecked,这是一个unsafe函数.使用unsafe块包围它,你可以在任何地方使用此代码.

与普通的Rust块一样,unsafe的值是其最后一个表达的值,或者()如果块没有.前面显示的对String::from_utf8_unchecked的调用提供了块的值.

一个unsafe块为你解锁了四个额外的选项:

  • 你可以调用unsafe函数.每个unsafe函数必须根据其目的指定自己的合约.

  • 你可以解引用原始指针.安全代码可以传递原始指针,比较它们,并通过从引用(甚至从整数)转换来创建它们,但只有不安全的代码才能实际使用它们来访问内存.我们将详细介绍原始指针,并在第538页的”Raw Pointers(原始指针)”中解释如何安全地使用它们.

  • 你可以访问可变的static变量.如第496页的”全局变量(Global Variables)”中所述,Rust无法确定线程何时使用可变的static变量,因此它们的合同要求你确保所有访问都已正确同步.

  • 你可以访问通过Rust的外部函数接口声明的函数和变量.即使在不可变的情况下,这些也被认为是unsafe的,因为它们对于用其他语言编写的代码是可见的,这些代码可能不符合Rust的安全规则.

将不安全的功能限制为unsafe块并不能真正阻止你做任何你想做的事情.完全可以将unsafe块粘贴到代码中并继续前进.规则的好处主要在于引起人们对安全Rust无法保证的代码的关注:

  • 你不会意外使用不安全的功能,然后发现你负责你甚至不知道存在的合同.

  • unsafe块吸引了评论者的更多关注.有些项目甚至具有自动化功能来确保这一点,标记代码更改会影响unsafe块以获得特别关注.

  • 当你考虑编写unsafe块时,你可以花点时间问自己,你的任务是否真的需要这些措施.如果是为了性能,你有测量结果表明这实际上是一个瓶颈吗?也许有一种很好的方法可以在安全的Rust中完成同样的事情.

示例:一个高效的ASCII字符串类型(Example: An Efficient ASCII String Type)

这是Ascii的定义,这是一种字符串类型,可确保其内容始终是有效的ASCII.此类型使用不安全的功能来提供零成本转换为String:

  1. mod my_ascii {
  2. use std::ascii::AsciiExt; // for u8::is_ascii
  3. /// An ASCII-encoded string.
  4. #[derive(Debug, Eq, PartialEq)]
  5. pub struct Ascii(
  6. // This must hold only well-formed ASCII text:
  7. // bytes from `0` to `0x7f`.
  8. Vec<u8>
  9. );
  10. impl Ascii {
  11. /// Create an `Ascii` from the ASCII text in `bytes`. Return a
  12. /// `NotAsciiError` error if `bytes` contains any non-ASCII
  13. /// characters.
  14. pub fn from_bytes(bytes: Vec<u8>) -> Result<Ascii, NotAsciiError> {
  15. if bytes.iter().any(|&byte| !byte.is_ascii()) {
  16. return Err(NotAsciiError(bytes))
  17. }
  18. Ok(Ascii(bytes))
  19. }
  20. }
  21. // When conversion fails, we give back the vector we couldn't convert.
  22. // This should implement `std::error::Error`; omitted for brevity.
  23. #[derive(Debug, Eq, PartialEq)]
  24. pub struct NotAsciiError(pub Vec<u8>);
  25. // Safe, efficient conversion, implemented using unsafe code.
  26. impl From<Ascii> for String {
  27. fn from(ascii: Ascii) -> String {
  28. // If this module has no bugs, this is safe, because
  29. // well-formed ASCII text is also well-formed UTF-8.
  30. unsafe { String::from_utf8_unchecked(ascii.0) }
  31. }
  32. }
  33. ...
  34. }

该模块的关键是Ascii类型的定义.类型本身标记为pub,以使其在my_ascii模块外部可见.但是类型的Vec<u8>元素 不是(not) 公有的,因此只有my_ascii模块可以构造Ascii值或引用其元素.这使模块的代码完全控制可能出现或未出现的内容.只要公有构造函数和方法确保新创建的Ascii值的格式良好并且在其整个生命中保持如此,那么程序的其余部分就不会违反该规则.实际上,公有构造函数Ascii::from_bytes在同意从中构造Ascii之前仔细检查它给出的向量.为简洁起见,我们没有显示任何方法,但你可以设想一组文本处理方法,确保Ascii值始终包含正确的ASCII文本,就像String的方法确保其内容保持格式良好的UTF-8.

这种安排让我们非常高效地为String实现From<Ascii>.不安全的函数String::from_utf8_unchecked接受一个字节向量并从中构建一个String,而不检查它的内容是否是格式正确的UTF-8文本;函数的契约让其调用者对此负责.幸运的是,Ascii类型强制执行的规则正是我们需要满足from_utf8_unchecked的合同.正如我们在第392页的”UTF-8(UTF-8)”中所解释的那样,任何ASCII文本块也都是格式良好的UTF-8,因此Ascii的底层Vec<u8>可以立即用作String的缓冲区.

有了这些定义,你可以写:

  1. use my_ascii::Ascii;
  2. let bytes: Vec<u8> = b"ASCII and ye shall receive".to_vec();
  3. // This call entails no allocation or text copies, just a scan.
  4. let ascii: Ascii = Ascii::from_bytes(bytes)
  5. .unwrap(); // We know these chosen bytes are ok.
  6. // This call is zero-cost: no allocation, copies, or scans.
  7. let string = String::from(ascii);
  8. assert_eq!(string, "ASCII and ye shall receive");

使用Ascii不需要unsafe块.我们使用不安全的操作实现了一个安全的接口,并且只根据模块自己的代码而不是用户的行为安排来满足他们的合同.

Ascii只不过是Vec<u8>的包装器,隐藏在一个模块中,该模块强制执行有关其内容的额外规则.这种类型称为 newtype ,Rust中的常见模式.Rust自己的String类型以完全相同的方式定义,只是它的内容被限制为UTF-8,而不是ASCII.实际上,这是标准库中String的定义:

  1. pub struct String {
  2. vec: Vec<u8>,
  3. }

在机器级别,在不显示Rust类型的情况下,newtype及其元素在内存中具有相同的表示,因此构造newtype根本不需要任何机器指令.在Ascii::from_bytes中,表达式Ascii(bytes)只是认为Vec<u8>的表示现在保持Ascii值.类似地,String::from_utf8_unchecked在内联时可能不需要机器指令:Vec<u8>现在被认为是一个String.

不安全函数(Unsafe Functions)

unsafe函数定义看起来像一个普通的函数定义,前面是unsafe关键字.unsafe函数的主体自动被视为unsafe块.

你可以仅在unsafe块中调用unsafe函数.这意味着标记函数unsafe会警告其调用者该函数具有必须满足的约定以避免未定义行为.

例如,这里是我们之前介绍的Ascii类型的新构造函数,它从字节向量构建Ascii,而不检查其内容是否是有效的ASCII:

  1. // This must be placed inside the `my_ascii` module.
  2. impl Ascii {
  3. /// Construct an `Ascii` value from `bytes`, without checking
  4. /// whether `bytes` actually contains well-formed ASCII.
  5. ///
  6. /// This constructor is infallible, and returns an `Ascii` directly,
  7. /// rather than a `Result<Ascii, NotAsciiError>` as the `from_bytes`
  8. /// constructor does.
  9. ///
  10. /// # Safety
  11. ///
  12. /// The caller must ensure that `bytes` contains only ASCII
  13. /// characters: bytes no greater than 0x7f. Otherwise, the effect is
  14. /// undefined.
  15. pub unsafe fn from_bytes_unchecked(bytes: Vec<u8>) -> Ascii {
  16. Ascii(bytes)
  17. }
  18. }

据推测,调用Ascii::from_bytes_unchecked的代码已经知道手中的向量只包含ASCII字符,因此Ascii::from_bytes坚持执行的检查将浪费时间,并且调用者必须编写代码来处理Err的结果,它知道永远不会发生.Ascii::from_bytes_unchecked允许这样的调用者回避检查和错误处理.

但是Ascii类型定义上面的注释说:”这个模块中的任何内容都不允许将非ASCII字节引入Ascii值.”这不正是这个新的from_bytes_unchecked构造函数的作用吗?

不完全是:from_bytes_unchecked通过合同将它们传递给调用者来履行其义务.这个合同的存在使得标记这个函数unsafe是正确的:尽管函数本身不执行不安全的操作,但它的调用者必须遵守规则Rust不能自动强制执行以避免未定义行为.

你真的可以通过违反Ascii::from_bytes_unchecked的合同来导致未定义行为吗?是的.你可以构造一个包含格式错误的UTF-8的String,如下所示:

  1. // Imagine that this vector is the result of some complicated process
  2. // that we expected to produce ASCII. Something went wrong!
  3. let bytes = vec![0xf7, 0xbf, 0xbf, 0xbf];
  4. let ascii = unsafe {
  5. // This unsafe function's contract is violated
  6. // when `bytes` holds non-ASCII bytes.
  7. Ascii::from_bytes_unchecked(bytes)
  8. };
  9. let bogus: String = ascii.into();
  10. // `bogus` now holds ill-formed UTF-8. Parsing its first character
  11. // produces a `char` that is not a valid Unicode code point.
  12. assert_eq!(bogus.chars().next().unwrap() as u32, 0x1fffff);

这说明了有关bugs和不安全代码的两个关键事实:

  • unsafe块之前发生的bugs可能会破坏合同(Bugs that occur before the unsafe block can break contracts) .unsafe块是否导致未定义行为不仅取决于块本身的代码,还取决于提供其操作的值的代码.你的unsafe代码依赖于满足合同的一切都是安全至关重要的.仅当模块的其余部分正确维护Ascii的不变量时,才能很好地定义基于String::from_utf8_uncheckedAsciiString的转换.

  • 违约的后果可能会出现在离开unsafe块之后(the consequences of breaking a contract may appear after you leave the unsafe block) .未遵守不安全功能合同所引起的未定义行为通常不会发生在unsafe块内.如前所示构造伪造的String可能不会导致问题,直到程序执行的后期.

从本质上讲,Rust的类型检查器,借用检查器和其他静态检查正在检查你的程序并尝试构建一个证明,证明它不能表现出未定义行为.当Rust成功编译你的程序时,这意味着它成功地证明了你的代码声音.一个unsafe块是这个证明中的一个空白:”这段代码,”你对Rust说,”很好,相信我.”你的主张是否真实可能取决于程序的任何部分,它们影响unsafe块中发生的事情,并且出现错误的后果可能会出现在任何受unsafe块影响的地方.编写unsafe关键字相当于提醒你,你没有从语言的安全检查中获得全部好处.

鉴于选择,你自然应该更喜欢创建安全的接口,而无需合同.这些更容易使用,因为用户可以依靠Rust的安全检查来确保他们的代码没有未定义行为.即使你的实现使用不安全的功能,也最好使用Rust的类型,生命周期和模块系统来满足它们的合同,同时只使用你自己可以保证的内容,而不是将责任传递给你的调用者.

不幸的是,在野外遇到不安全函数并不罕见,其文档无需解释它们的合同.根据你的经验和代码行为的知识,你需要自己推断规则.如果你曾经不安地想知道你使用C或C++ API做的是否正常,那么你就知道它是什么样的.

不安全块还是不安全函数?(Unsafe Block or Unsafe Function?)

你可能会发现自己想知道是使用unsafe块还是只是标记整个函数不安全.我们建议的方法是先做出关于函数的决定:

  • 如果以可编译的方式滥用函数但仍导致未定义行为,则必须将其标记为不安全.正确使用该函数的规则是其合同;合同的存在是使函数不安全的原因.

  • 否则,该函数是安全的:没有对其进行良好类型的调用会导致未定义行为.它不应该被标记为unsafe.

函数是否在其主体中使用不安全的功能是无关紧要的;重要的是合同的存在.之前,我们展示了一个不安全函数,它不使用不安全的功能,以及一个使用不安全功能的安全函数.

不要因为在体使用不安全的功能而将安全函数标记为unsafe.这使得该函数更难以使用,并且会使(正确地)期望找到合同解释的读者感到困惑.相反,使用unsafe块,即使它是函数的整个主体.

未定义行为(Undefined Behavior)

在介绍中,我们说术语 未定义行为(undefined behavior) 意味着”Rust坚定地认为你的代码永远不会表现出来的行为(behavior that Rust firmly assumes your code could never exhibit).”这是一个奇怪的短语转变,特别是因为我们从其他语言的经验中知道这些行为 确实(do) 是偶然发生的,有一定频率.为什么这个概念有助于规定不安全代码的义务?

编译器是从一种编程语言到另一种编程语言的翻译器.Rust编译器接受Rust程序并将其转换为等效的机器语言程序.但是,说这种完全不同语言的两个程序是等价的是什么意思呢?

幸运的是,对于程序员而言,这个问题比语言学家更容易.我们通常说两个程序是等价的,如果它们在执行时总是具有相同的可见行为:它们进行相同的系统调用,以相同的方式与外部库交互,等等.这有点像程序的图灵测试:如果你不能分辨你是在与原始的还是翻译的进行交互,那么它们就是等价的.

现在考虑以下代码:

  1. let i = 10;
  2. very_trustworthy(&i);
  3. println!("{}", i * 100);

即使对于very_trustworthy的定义一无所知,我们也可以看到它只接收到对i的共享引用,因此调用不能改变i的值.由于传递给println!的值将始终为1000,Rust可以将此代码转换为机器语言,就像我们这样写的一样:

  1. very_trustworthy(&10);
  2. println!("{}", 1000);

此转换版本具有与原始版本相同的可见行为,并且可能更快一些.但只有在我们同意它与原版具有相同含义的情况下考虑此版本的性能才是有意义的.如果very_trustworthy的定义如下?

  1. fn very_trustworthy(shared: &i32) {
  2. unsafe {
  3. // Turn the shared reference into a mutable pointer.
  4. // This is undefined behavior.
  5. let mutable = shared as *const i32 as *mut i32;
  6. *mutable = 20;
  7. }
  8. }

此代码违反了共享引用的规则:它将i的值更改为20,即使它应该被冻结,因为i是共享借用.结果,我们对调用者的转换现在具有非常明显的效果:如果Rust转换代码,程序将打印1000;如果它单独留下代码并使用i的新值,则打印2000.在very_trustworthy中打破共享引用的规则意味着共享引用在其调用者中不会按预期运行.

这种问题出现在Rust可能尝试的几乎所有类型的转换中.甚至将函数内联到其调用点中也假设,当被调用者结束时,控制流返回到调用点.但是我们打开了这一章,其中包含了一个违反该假设的不良行为代码的例子.

Rust(或任何其他语言)基本上不可能评估程序的转换是否保留其含义,除非它可以信任该语言的基本功能以按设计行事.它们是否有所不同,不仅取决于手头的代码,还取决于其他(可能很遥远)的程序部分.为了对你的代码做任何事情,Rust必须假设你的程序的其余部分都表现良好.

这里是Rust的,对于表现良好的程序的规则:

  • 程序不得读取未初始化的内存.

  • 程序不得创建无效的原始值:

    • 引用或boxes为bull
    • bool值不是01
    • 具有无效判别值的enum
    • 无效的char值,无代码的Unicode代码点
    • str值不是格式良好的UTF-8
  • 必须遵守第5章中解释的引用规则.没有任何引用可以活得比它引用的对象更久;共享访问是只读访问;并且可变访问是独占访问.

  • 程序不得解引用null,错误对齐或悬空指针.

  • 程序不得使用指针访问与指针关联的分配之外的内存.我们将在第540页的”安全地解引用原始指针(Dereferencing Raw Pointers Safely)”中详细解释此规则.

  • 程序必须没有数据竞争.当两个线程在没有同步的情况下访问相同的内存位置时,会发生数据竞争,并且至少有一个访问是写入.

  • 程序不得通过外部函数接口,在另一种语言的调用中展开,如第146页的”展开(Unwinding)”中所述.

  • 程序必须符合标准库函数的合同.

这些规则都是Rust在优化程序并将其转换为机器语言的过程中所假设的.简单来说,未定义行为违反了这些规则.这就是为什么我们说Rust假定你的程序不会出现未定义行为:如果我们希望得出结论编译的程序是源代码的忠实翻译,那么这个假设是必要的.

不使用不安全功能的Rust代码保证在编译后遵循所有前面的规则.一旦其编译.只有当你使用不安全的功能时,这些规则才会成为你的责任.在C和C++中,程序编译时没有错误或警告意味着更少;正如我们在本书的介绍中所提到的那样,即使是那些将代码保持在高标准的备受尊重的项目所编写的最好的C和C++程序在实践中也表现出不未定义行为.

不安全Traits(Unsafe Traits)

不安全trait(unsafe trait) 是具有合同的trait,Rust无法检查或强制实现者必须满足以避免未定义行为.要实现不安全trait,必须将实现标记为不安全.由你来理解trait的合同,并确保你的类型满足它.

用不安全trait限制其类型变量的函数通常是使用不安全的功能的函数,并且仅通过依赖于不安全trait的契约来满足其合同.trait的不正确实现可能导致此类函数表现出未定义行为.

不安全traits的典型例子是std::marker::Sendstd::marker::Sync.这些traits没有定义任何方法,因此对于你喜欢的任何类型实现它们都很简单.但它们确实有合同:Send要求实现者安全地移动到另一个线程,Sync要求它们通过共享引用安全地在线程之间共享.例如,为不合适的类型实现Send会使std::sync::Mutex不再对数据竞争安全.

举一个简单的例子,Rust库包含一个不安全的特性core::nonzero::Zeroable,用于可以通过将所有字节设置为0来安全地初始化的类型,很明显,将usize归零很好,但将&T归零会给你一个空引用,如果解引用会导致崩溃.对于可归零(zeroable)的类型,可以进行一些优化:你可以使用std::mem::write_bytes(Rust的memset等价物)快速初始化它们的数组,或者使用分配归零页面的操作系统调用.(从Rust 1.17开始,Zeroable是实验性的,所以它可能会在Rust的未来版本中被更改或删除,但它是一个好的,简单的,真实的例子.)

Zeroable是典型的标记trait,缺少方法或关联类型:

  1. pub unsafe trait Zeroable {}

适当类型的实现同样简单明了:

  1. unsafe impl Zeroable for u8 {}
  2. unsafe impl Zeroable fori32 {}
  3. unsafe impl Zeroable for usize {}
  4. // and so on for all the integer types

有了这些定义,我们可以编写一个函数,快速分配包含Zeroable类型的给定长度的向量:

  1. #![feature(nonzero)]
  2. // permits `Zeroable`
  3. extern crate core;
  4. use core::nonzero::Zeroable;
  5. fn zeroed_vector<T>(len: usize) -> Vec<T>
  6. where T: Zeroable
  7. {
  8. let mut vec = Vec::with_capacity(len);
  9. unsafe {
  10. std::ptr::write_bytes(vec.as_mut_ptr(), 0, len);
  11. vec.set_len(len);
  12. }
  13. vec
  14. }

此函数首先创建一个具有所需容量的空Vec,然后调用write_bytes以用零填充未占用的缓冲区.(write_byte函数将len视为T元素的个数,而不是字节的个数,因此该调用会填充整个缓冲区.)向量的set_len方法更改其长度而不对缓冲区执行任何操作;这是不安全的,因为你必须确保新封闭的缓冲区空间实际上包含类型为T的正确初始化值.但这正是T: Zeroable限制所建立的:零字节块表示有效的T值.我们使用set_len是安全的.

在这里,我们使用它:

  1. let v: Vec<usize> = zeroed_vector(100_000);
  2. assert!(v.iter().all(|&u| u == 0));

显然,Zeroable必须是不安全trait,因为不遵守其合同的实现可能导致未定义行为:

  1. struct HoldsRef<'a>(&'a mut i32);
  2. unsafe impl<'a> Zeroable for HoldsRef<'a> { }
  3. let mut v: Vec<HoldsRef> = zeroed_vector(1);
  4. *v[0].0 = 1; // crashes: dereferences null pointer

Rust在没有抱怨的情况下编译它:它不知道Zeroable意味着什么,所以它无法判断它何时被用于不适当的类型.与任何其他不安全功能一样,由你来理解并遵守不安全trait的合同.

请注意,不安全的代码不能依赖于正确实现的普通,安全特性.例如,假设有一个std::hash::Hashertrait的实现,它只返回一个随机哈希值,与被哈希的值无关.该trait要求对两次相同的位进行哈希必须产生相同的哈希值,但此实现不符合该要求;这完全是错误的.但是因为Hasher不是一个不安全trait,所以当使用这个hasher时,不安全代码不得表现出未定义行为.std::collections::HashMap类型是经过精心编写的,以尊重它使用的不安全功能的合同,无论hasher的行为如何.当然,该表将无法正常运行:查找将失败,并且条目将随机出现和消失.但该表不会显示未定义行为.

原始指针(Raw Pointers)

Rust中的 原始指针(raw pointer) 是一个不受约束的指针.你可以使用原始指针来形成Rust的检查指针类型不能的各种结构,例如双向链表或对象的任意图形.但由于原始指针非常灵活,Rust无法判断你是否安全使用它们,因此你只能在unsafe块中解引用它们.

原始指针本质上等同于C或C++指针,因此它们对于与用这些语言编写的代码进行交互也很有用.有两种原始指针:

  • *mut T是指向T的原始指针,允许修改其引用的对象.

  • *const T是指向T的原始指针,只允许读取其引用的对象.

(没有普通的*T类型;你必须始终指定constmut.)

你可以通过从引用转换来创建原始指针,并使用*运算符解引用它:

  1. let mut x = 10;
  2. let ptr_x = &mut x as *mut i32;
  3. let y = Box::new(20);
  4. let ptr_y = &*y as *const i32;
  5. unsafe {
  6. *ptr_x += *ptr_y;
  7. }
  8. assert_eq!(x, 30);

与boxes和引用不同,原始指针可以为空(null),如C中的NULL或C++中的nullptr:

  1. fn option_to_raw<T>(opt: Option<&T>) -> *const T {
  2. match opt {
  3. None => std::ptr::null(),
  4. Some(r) => r as *const T
  5. }
  6. }
  7. assert!(!option_to_raw(Some(&("pea", "pod"))).is_null());
  8. assert_eq!(option_to_raw::<i32>(None), std::ptr::null());

这个例子没有unsafe块:创建原始指针,传递它们,并比较它们都是安全的.只有解引用原始指针是不安全的.

指向无大小的(unsized)类型的原始指针是胖指针,就像相应的引用或Box类型一样.一个*const [u8]指针包含一个长度和地址,一个trait对象如*mut std::io::Write指针携带一个虚表(vtable).

虽然Rust在各种情况下隐式地解引用安全指针类型,但原始指针解引用必须是显式的:

  • .运算符不会隐式解引用原始指针;你必须写(*raw).field(*raw).method(...).

  • 原始指针不实现Deref,因此解引用强制(deref coercions)不适用于它们.

  • 运算符如==<将原始指针作为地址进行比较:如果两个原始指针指向内存中的相同位置,则它们是相等的.类似地,哈希原始指针会哈希它指向的地址,而不是其引用的对象的值.

  • 格式化traits如std::fmt::Display会自动跟踪引用,但根本不处理原始指针.例外是std::fmt::Debugstd::fmt::Pointer,它们将原始指针显示为十六进制地址,而不解引用它们.

与C和C++中的+运算符不同,Rust的+不处理原始指针,但你可以通过其offsetwrapping_offset方法执行指针运算.找到两个指针之间的距离没有标准操作,就像-运算符在C和C++中那样,但你可以自己编写一个:

  1. fn distance<T>(left: *const T, right: *const T) -> isize {
  2. (left as isize - right as isize) / std::mem::size_of::<T>() as isize
  3. }
  4. let trucks = vec!["garbage truck", "dump truck", "moonstruck"];
  5. let first = &trucks[0];
  6. let last = &trucks[2];
  7. assert_eq!(distance(last, first), 2);
  8. assert_eq!(distance(first, last), -2);

即使distance的参数是原始指针,我们也可以将引用传递给它:Rust隐含地强制引用原始指针(当然不是反过来).

as运算符允许从引用到原始指针或两个原始指针类型之间的几乎所有合理的转换.但是,你可能需要将复杂的转换分解为一系列简单的步骤.例如:

  1. &vec![42_u8] as *const String// error: invalid conversion
  2. &vec![42_u8] as *const Vec<u8> as *const String; // permitted

请注意,as不会将原始指针转换为引用.这样的转换将是不安全的,as应该保持安全操作.相反,你必须解引用原始指针(在unsafe块中),然后借用结果值.

执行此操作时要非常小心:以这种方式生成的引用具有不受约束的生命周期:它可以存活多长时间没有限制,因为原始指针使得Rust无法做出这样的决定.在本章后面的第572页的”libgit2的安全接口(A Safe Interface to libgit2)”中,我们将展示如何正确约束生命周期的几个示例.

许多类型都有as_ptras_mut_ptr方法,它们返回指向其内容的原始指针.例如,数组切片和字符串返回指向其第一个元素的指针,一些迭代器返回指向它们将生成的下一个元素的指针.像Box,RcArc这样的拥有指针类型有into_rawfrom_raw函数,可以转换为原始指针和从原始指针转换来.其中一些方法的合同带来了令人惊讶的要求,因此在使用它们之前请查阅它们的文档.

你也可以通过从整数转换来构造原始指针,尽管你可以信任的唯一整数通常是你首先从指针获得的整数.第541页的”示例:RefWithFlag(Example: RefWithFlag)”以这种方式使用原始指针.

与引用不同,原始指针既不是Send也不是Sync.因此,默认情况下,包含原始指针的任何类型都不会实现这些traits.在线程之间发送或共享原始指针没有什么本质上不安全的;毕竟,无论它们走到哪里,你仍然需要一个unsafe块来解引用它们.但是考虑到原始指针通常扮演的角色,语言设计者认为这种行为是更有用的默认行为.我们已经在第536页的”不安全Traits(Unsafe Traits)”中讨论了如何自己实现SendSync.

安全地解引用原始指针(Dereferencing Raw Pointers Safely)

以下是一些安全使用原始指针的常识性指南:

  • 解引用空指针或悬空指针是未定义行为,因为指向未初始化的内存或超出作用域的值.

  • 解引用未针对其引用的对象类型正确对齐的指针是未定义行为.

  • 只有在遵守第5章中解释的引用安全规则的情况下,你才可以从解引用的原始指针借用值:没有引用可能比它的引用的对象活得更久;共享访问是只读访问;可变访问是独占访问.(此规则很容易被违反,因为原始指针通常用于创建具有非标准共享或所有权的数据结构.)

  • 只有当它是格式良好的其类型的值时,才可以使用原始指针的引用的对象.例如,你必须确保解引用*const char会产生正确的,非代理(nonsurrogate)的Unicode代码点.

  • 你可以在原始指针上使用offsetwrapping_offset方法,仅指向原始指针所引用的变量或堆分配的内存块中的字节,或指向此区域之外的第一个字节.

如果通过将指针转换为整数来进行指针运算,对整数进行算术运算,然后将其转换回指针,则结果必须是offset方法的规则允许你生成的指针.

  • 如果赋值给原始指针的引用对象,则不得违反引用对象所属的任何类型的不变量.例如,如果你有一个*mut u8指向一个String的一个字节,你可能只存储u8中的值,使得String保持格式良好的UTF-8.

除了借用规则,这些规则与在C或C++中使用指针时必须遵循的规则基本相同.

不违反类型不变量的原因应该是清楚的.Rust的许多标准类型在其实现中使用不安全代码,但仍然提供安全的接口,假设Rust的安全检查,模块系统和可见性规则将得到尊重.使用原始指针来规避这些保护措施可能导致未定义行为.

原始指针的完整,准确的合约不容易说明,并且可能随着语言的发展而变化.但是这里列出的原则应该让你处于安全的境地.

示例:RefWithFlag(Example: RefWithFlag)

下面是一个示例,说明如何通过原始指针实现经典^1的位级别的hack,并将其包装为完全安全的Rust类型.这个模块定义了一个类型,RefWithFlag<'a, T>,它同时包含一个&'a T和一个bool,就像元组`(&’a T, bool),但仍然设法只占用一个机器字而不是两个.这种技术经常用于垃圾收集器和虚拟机,其中某些类型—如表示对象的类型—如此之多,以至于每个值添加单个字会大大增加内存使用量:

  1. mod ref_with_flag {
  2. use std::marker::PhantomData;
  3. use std::mem::align_of;
  4. /// A `&T` and a `bool`, wrapped up in a single word.
  5. /// The type `T` must require at least two-byte alignment.
  6. ///
  7. /// If you're the kind of programmer who's never met a pointer whose
  8. /// 2⁰-bit you didn't want to steal, well, now you can do it safely!
  9. /// ("But it's not nearly as exciting this way...")
  10. pub struct RefWithFlag<'a, T: 'a> {
  11. ptr_and_bit: usize,
  12. behaves_like: PhantomData<&'a T> // occupies no space
  13. }
  14. impl<'a, T: 'a> RefWithFlag<'a, T> {
  15. pub fn new(ptr: &'a T, flag: bool) -> RefWithFlag<T> {
  16. assert!(align_of::<T>() % 2 == 0);
  17. RefWithFlag {ptr_and_bit: ptr as *const T asusize | flag as usize,
  18. behaves_like: PhantomData
  19. }
  20. }
  21. pub fn get_ref(&self) -> &'a T {
  22. unsafe {
  23. let ptr = (self.ptr_and_bit & !1) as *const T;
  24. &*ptr
  25. }
  26. }
  27. pub fn get_flag(&self) -> bool {
  28. self.ptr_and_bit & 1 != 0
  29. }
  30. }
  31. }

这段代码利用了许多类型必须放在内存中偶数地址的事实:因为偶数地址的最低有效位总是为零,我们可以在那里存储其他东西,然后通过屏蔽底位可靠地重建原始地址位.并非所有类型都符合条件;例如,类型u8(bool, [i8; 2])可以放在任何地址.但是我们可以在构造时检查类型的对齐情况,拒绝不起作用的类型.

你可以像这样使用RefWithFlag:

  1. use ref_with_flag::RefWithFlag;
  2. let vec = vec![10, 20, 30];
  3. let flagged = RefWithFlag::new(&vec, true);
  4. assert_eq!(flagged.get_ref()[1], 20);
  5. assert_eq!(flagged.get_flag(), true);

构造函数RefWithFlag::new接受引用和bool值,断言引用的类型是合适的,然后将引用转换为原始指针,然后转换为usize.usize类型被定义为足够大以在我们正在编译的任何处理器上保存指针,因此将原始指针转换为usize和转回是良好定义的.一旦我们有了一个usize,我们知道它必须是偶数,所以我们可以使用|按位或运算符将它与bool组合,我们已将其转换为整数01.

get_flag方法提取RefWithFlagbool组件.这很简单:只需掩盖底位并检查它是否非零.

get_ref方法从RefWithFlag中提取引用.首先,它掩盖了usize的底位并将其转换为原始指针.as运算符不会将原始指针转换为引用,但是我们可以解引用原始指针(当然是在一个unsafe块中)并借用它.借用一个原始指针的引用对象为你提供一个无限生命周期的引用:Rust会给出引用,无论生命周期会使它周围的代码检查,如果有的话.但是,通常情况下,某些特定的生命周期更准确,因此会出现更多错误.在这种情况下,由于get_ref的返回类型是&'a T,Rust推断引用的生命周期必须是RefWithFlag的参数,这正是我们想要的:这是我们开始的引用的生命周期.

在内存中,RefWithFlag看起来就像一个usize:由于PhantomData是一个零大小的类型,所以behaves_like字段在结构中不占用空间.但是,PhantomData是Rust知道如何在使用RefWithFlag的代码中处理生命周期的必要条件.想象一下没有behaves_like字段时类型会是什么样子:

  1. // This won't compile.
  2. pub struct RefWithFlag<'a, T: 'a> {
  3. ptr_and_bit: usize
  4. }

在第5章中,我们指出任何包含引用的结构都不得活得比它们借用的值更久,以免引用成为悬空指针.结构必须遵守适用于其字段的限制.这当然适用于RefWithFlag:在我们刚看到的示例代码中,flagged不得活得比vec久,因为flagged.get_ref()返回对它的引用.但是我们减少的RefWithFlag类型根本不包含任何引用,并且从不使用它的生命周期参数'a.它只是一个usize.Rust怎么知道任何限制都适用于pab的生命周期?包括一个PhantomData<&'a T>字段告诉Rust将RefWithFlag<'a, T>视为就好像(as if) 它包含&'a T,而不实际影响结构的表示.

虽然Rust并不真正知道发生了什么(这就是让RefWithFlag不安全的原因),但它会尽力帮助你解决这个问题.如果省略_marker字段,Rust会抱怨参数'aT未使用,并建议使用PhantomData.

RefWithFlag使用与我们之前介绍的Ascii类型相同的策略,以避免其unsafe块中的未定义行为.类型本身是pub,但它的字段不是,,意味着只有pointer_and_bool模块中的代码可以创建或查看RefWithFlag值.你不必检查太多代码就可以确信ptr_and_bit字段构造良好.

可空的指针(Nullable Pointers)

Rust中的空(null)原始指针是一个零地址,就像在C和C++中一样.对于任何类型T,std::ptr::null<T>函数返回*const T空指针,std::ptr::null_mut<T>返回*mut T空指针.

有几种方法可以检查原始指针是否为空.最简单的是is_null方法,但是as_ref方法可能更方便:它接受*const T指针并返回Option<&'a T>,将空指针转换为None.类似地,as_mut方法将*mut T指针转换为Option<&'a mut T>值.

类型大小和对齐(Type Sizes and Alignments)

任何Sized类型的值在内存中占用恒定的字节数,并且必须放置在由机器体系结构确定的某个 对齐(alignment) 值的倍数的地址处.例如,(i32, i32)元组占用8个字节,并且大多数处理器更喜欢将其放置在4的倍数的地址处.

调用std::mem::size_of::<T>()返回类型为T的值的大小(以字节为单位),std::mem::align_of::<T>()返回其所需的对齐.例如:

  1. assert_eq!(std::mem::size_of::<i64>(), 8);
  2. assert_eq!(std::mem::align_of::<(i32, i32)>(), 4);

任何类型的对齐总是2的幂.

类型的大小总是向上舍入到其对齐的倍数,即使它在技术上可以适合更小的空间.例如,即使像(f32, u8)这样的元组只需要五个字节,size_of::<(f32, u8)>()也是8,因为align_of::<(f32, u8)>()4.这可确保如果你有数组,则元素类型的大小始终反映一个元素与下一个元素之间的间距.

对于无大小的类型,大小和对齐取决于手头的值.给定对无大小的值的引用,std::mem::size_of_valstd::mem::align_of_val函数返回值的大小和对齐.这些函数可以对Sized和无大小类型的引用进行操作.

  1. // Fat pointers to slices carry their referent's length.
  2. let slice: &[i32] = &[1, 3, 9, 27, 81];
  3. assert_eq!(std::mem::size_of_val(slice), 20);
  4. let text: &str = "alligator";
  5. assert_eq!(std::mem::size_of_val(text), 9);
  6. use std::fmt::Display;
  7. let unremarkable: &Display = &193_u8;
  8. let remarkable: &Display = &0.0072973525664;
  9. // These return the size/alignment of the value the
  10. // trait object points to, not those of the trait object
  11. // itself. This information comes from the vtable the
  12. // trait object refers to.
  13. assert_eq!(std::mem::size_of_val(unremarkable), 1);
  14. assert_eq!(std::mem::align_of_val(remarkable), 8);

指针运算(Pointer Arithmetic)

Rust将数组,切片或向量的元素作为单个连续的内存块进行布局,如图21-1所示.元素是规则间隔的,因此如果每个元素占用size字节,则第i个元素以第i * size字节开始.

图21-1. 内存种的数组.

这样做的一个好结果是,如果你有两个指向数组元素的原始指针,比较指针会得到与比较元素索引相同的结果:如果i < j,则指向第i个元素的原始指针小于指向第j个元素的原始指针.这使得原始指针可用作数组遍历的边界.实际上,标准库在切片上的简单迭代器定义如下:

  1. struct Iter<'a, T: 'a> {
  2. ptr: *const T,
  3. end: *const T,
  4. ...
  5. }

ptr字段指向迭代应该产生的下一个元素,并且end字段用作限制:当ptr == end时,迭代完成.

数组布局的另一个好结果:如果element_ptr是一个*const T*mut T原始指针,它指向某个数组的第i个元素,那么element_ptr.offset(o)是一个指向第(i + o)个元素的原始指针.它的定义等同于:

  1. fn offset(self: *const T, count: isize) -> *const T
  2. where T: Sized
  3. {
  4. let bytes_per_element = std::mem::size_of::<T>() as isize;
  5. let byte_offset = count * bytes_per_element;
  6. (self as isize).checked_add(byte_offset).unwrap() as *const T
  7. }

std::mem::size_of::<T>函数以字节为单位返回类型T的大小.根据定义,isize足够大以容纳地址,你可以将基指针转换为isize,对该值进行算术运算,然后将结果转换回指针.

在数组结束后生成指向第一个字节的指针是很好的.你不能解引用这样的指针,但它可以用于表示循环的限制或用于边界检查.

但是,使用offset来生成超出该点的,或者在数组开始之前的指针,即使你从未解引用它,也是未定义的行为.为了优化,Rust想假设当i是正数时,ptr.offset(i) > ptr,而当i是负数时,ptr.offset(i) < ptr.这个假设似乎是安全的,但如果offset中的算术溢出isize值,它可能不成立.如果i被约束为与ptr保持在同一个数组中,则不会发生溢出:毕竟,数组本身不会溢出地址空间的边界.(要使指向结束之后的第一个字节安全,Rust永远不会将值放在地址空间的上端.)

如果确实需要将指针偏移超出与它们关联的数组的限制,则可以使用wrapping_offset方法.这相当于offset,但Rust没有假设ptr.wrapping_offset(i)ptr本身的相对排序.当然,除非它们属于数组,否则你仍然无法解引用这些指针.

移入和移出内存(Moving into and out of Memory)

如果要实现管理自己内存的类型,则需要跟踪内存的哪些部分保存活着的值,哪些部分未初始化,就像Rust处理局部变量一样.考虑以下代码:

  1. let pot = "pasta".to_string();
  2. let plate;
  3. plate = pot;

运行此代码后,情况如图21-2所示.

图21-2. 将字符串从一个局部变量移动到另一个局部变量.

赋值后,pot是未初始化的,plate是字符串的所有者.

在机器级别,没有指定移动对源的作用,但在实践中,它通常什么都不做.该赋值可能使pot仍然保持字符串的指针,容量和长度.当然.将此视为活着的值将是灾难性的,Rust确保你不这样做.

同样的考虑适用于管理自己内存的数据结构.假设你运行此代码:

  1. let mut noodles = vec!["udon".to_string()];
  2. let soba = "soba".to_string();
  3. let last;

在内存中,状态如图21-3所示.

图21-3. 一个具有未初始化的,备用容量的向量.

向量具有容纳一个元素的备用容量,但其内容是垃圾,可能是先前存储的内存.假设你然后运行此代码:

  1. noodles.push(soba);

将字符串压入向量会将未初始化的内存转换为新元素,如图21-4所示:

图21-4. 将soba的值推入向量上之后.

向量已初始化其空白空间以拥有该字符串,并增加其长度以将其标记为新的,活着的元素.向量现在是字符串的所有者;你可以引用它的第二个元素,并且删除向量将释放两个字符串.soba现在是未初始化的.

最后,考虑一下当我们从向量中弹出一个值时会发生什么:

  1. last = noodles.pop().unwrap();

在内存中,现在看起来如图21-5所示.

图21-5.将一个元素从向量弹出到last之后.

变量last取得了字符串的所有权.向量已减少其长度,以指示用于保存字符串的空间现在未初始化.

就像之前的potpasta一样,所有三个soba,last,以及向量的自由空间可能具有相同的位模式.但只有last才被认为拥有这个值.将其他两个位置中的任何一个视为活着的将是一个错误.

初始化值的真正定义是值 被视为活着的(treated as live) .写入值的字节通常是初始化的必要部分,但这只是因为这样做会将值视为活着的.

Rust在编译时跟踪局部变量.像Vec,HashMap,Box等类型动态跟踪它们的缓冲区.如果实现管理自己内存的类型,则需要执行相同的操作.

Rust为实现这些类型提供了两个基本操作:

  • std::ptr::read(src)将值移出src指向的位置,将所有权转移给调用者.调用read后,必须将*src视为未初始化的内存.src参数应该是*const T原始指针,其中T是一个有大小的类型.

这是Vec::pop背后的操作.弹出一个值会调用read将值移出缓冲区,然后递减长度以将该空间标记为未初始化的容量.

  • std::ptr::write(dest, value)value移动到dest指向的位置,在调用之前必须是未初始化的内存.引用的对象现在拥有该值.这里,dest必须是*mut T原始指针并且valueT值,其中T是有大小的类型.

这是Vec::push背后的操作.推入值会调用write以将值移动到下一个可用空间,然后递增长度以将该空间标记为有效元素.

两者都是自由函数,而不是原始指针类型的方法.

请注意,你不能使用Rust的任何安全指针类型来执行这些操作.它们都需要在任何时候初始化它们的引用对象,因此将未初始化的内存转换为值(反之亦然),是他们无法实现的.原始指针符合要求.

标准库还提供了将值的数组从一个内存块移动到另一个内存块的函数:

  • std::ptr::copy(src, dst, count)将从src开始的内存中的count值的数组移动到dst的内存中,就好像你已经编写了一个readwrite调用循环来移动它们,一次一个.在调用之前,目标内存必须是未初始化的,之后源内存将保持未初始化状态.srcdest参数必须是*const T*mut T原始指针,count必须是usize.

  • std::ptr::copy_nonoverlapping(src, dst, count)就像对应的copy调用一样,只是它的契约还要求内存的源块和目标块不能重叠.这可能比调用copy稍快.

还有另外两个readwrite函数系列,也在std::ptr模块中:

  • read_unalignedwrite_unaligned函数类似于readwrite,只是指针不需要像引用对象类型通常所需的那样对齐.这些函数可能比普通的readwrite函数慢.

  • read_volatilewrite_volatile函数相当于C或C++中的易失性(volatile)读写.

示例: GapBuffer(Example: GapBuffer)

这是一个将刚刚描述的原始指针函数投入使用的示例.

假设你正在编写文本编辑器,你正在寻找一种表示文本的类型.你可以选择String,并使用insertremove方法在用户键入时插入和删除字符.但是如果他们在大文件的开头编辑文本,那么这些方法可能很昂贵:插入一个新字符需要将字符串的其余部分移到内存中的右边,删除将它全部移回到左边.你希望这种常见的操作更便宜.

Emacs文本编辑器使用一种称为 间隙缓冲区(gap buffer) 的简单数据结构,可以在恒定时间内插入和删除字符.尽管String在文本末尾保留了所有备用容量,这使得pushpop便宜,但是在编辑正在发生的时候,间隙缓冲区将其备用容量保持在文本中间.这个备用容量称为 间隙(gap) .在间隙处插入或删除元素很便宜:你只需根据需要缩小或扩大间隙.你可以通过将文本从间隙的一侧移动到另一侧来将间隙移动到你喜欢的任何位置.当间隙为空时,你将迁移到更大的缓冲区.

虽然间隙缓冲区中的插入和删除速度很快,但更改它们发生的位置需要将间隙移动到新位置.移动元素需要的时间与移动距离成比例.幸运的是,典型的编辑活动需要在离开缓冲区之前对缓冲区的一个邻域中进行一系列更改,然后在其他地方修改文本.

在本节中,我们将在Rust中实现间隙缓冲区.为了避免被UTF-8分心,我们将直接使缓冲区存储char值,但如果我们以其他形式存储文本,则操作原理将是相同的.

首先,我们将展示一个运行种的间隙缓冲区.此代码创建一个GapBuffer,在其中插入一些文本,然后将插入点移动到最后一个单词之前:

  1. use gap::GapBuffer;
  2. let mut buf = GapBuffer::new();
  3. buf.insert_iter("Lord of the Rings".chars());
  4. buf.set_position(12);

运行此代码后,缓冲区如图21-6所示.

图21-6. 包含一些文本的间隙缓冲区.

插入是用新文本填补间隙的问题.这段代码添加了一个单词并破坏了电影(原单词:lord of the rings(指环王))):

  1. buf.insert_iter("Onion ".chars());

图21-7. 包含更多文本的间隙缓冲区.

这是我们的GapBuffer类型:

  1. mod gap {
  2. use std;
  3. use std::ops::Range;
  4. pub struct GapBuffer<T> {
  5. // Storage for elements. This has the capacity we need, but its length
  6. // always remains zero. GapBuffer puts its elements and the gap in this
  7. // `Vec`'s "unused" capacity.
  8. storage: Vec<T>,
  9. // Range of uninitialized elements in the middle of `storage`.
  10. // Elements before and after this range are always initialized.
  11. gap: Range<usize>
  12. }
  13. ...
  14. }

GapBuffer以奇怪的方式^2使用其存储字段.它实际上从未在向量中存储任何元素—或者不完全存储.它只是调用Vec::with_capacity(n)来获取足够大的内存块以容纳n个值,通过向量的as_ptras_mut_ptr方法获得指向该内存的原始指针,然后直接使用缓冲区用于其自身目的.向量的长度始终为零.当Vec被删除时,Vec不会试图释放它的元素,因为它不知道它有任何,但它确实释放了内存块.这就是GapBuffer想要的;它有自己的Drop实现,它知道活着的元素的位置并正确删除它们.

GapBuffer最简单的方法就是你所期望的:

  1. impl<T> GapBuffer<T> {
  2. pub fn new() -> GapBuffer<T> {
  3. GapBuffer { storage: Vec::new(), gap: 0..0 }
  4. }
  5. /// Return the number of elements this GapBuffer could hold without
  6. /// reallocation.
  7. pub fn capacity(&self) -> usize {
  8. self.storage.capacity()
  9. }
  10. /// Return the number of elements this GapBuffer currently holds.
  11. pub fn len(&self) -> usize {
  12. self.capacity() - self.gap.len()
  13. }
  14. /// Return the current insertion position.
  15. pub fn position(&self) -> usize {
  16. self.gap.start
  17. }
  18. ...
  19. }

它清除了以下许多函数,以便有一个工具方法,它返回给定索引处的缓冲区元素的原始指针.这是Rust,我们最终需要一个用于mut指针的方法和一个用于const指针的方法.与前面的方法不同,这些方法不公有.继续这个impl块:

  1. /// Return a pointer to the `index`'th element of the underlying storage,
  2. /// regardless of the gap.
  3. ///
  4. /// Safety: `index` must be a valid index into `self.storage`.
  5. unsafe fn space(&self, index: usize) -> *const T {
  6. self.storage.as_ptr().offset(index as isize)
  7. }
  8. /// Return a mutable pointer to the `index`'th element of the underlying
  9. /// storage, regardless of the gap.
  10. ///
  11. /// Safety: `index` must be a valid index into `self.storage`.
  12. unsafe fn space_mut(&mut self, index: usize) -> *mut T {
  13. self.storage.as_mut_ptr().offset(index as isize)
  14. }

要在给定索引处查找元素,你必须考虑索引是在间隙之前还是之后,并进行适当调整:

  1. /// Return the offset in the buffer of the `index`'th element, taking
  2. /// the gap into account. This does not check whether index is in range,
  3. /// but it never returns an index in the gap.
  4. fn index_to_raw(&self, index: usize) -> usize {
  5. if index < self.gap.start {
  6. index
  7. } else {
  8. index + self.gap.len()
  9. }
  10. }
  11. /// Return a reference to the `index`'th element,
  12. /// or `None` if `index` is out of bounds.
  13. pub fn get(&self, index: usize) -> Option<&T> {
  14. let raw = self.index_to_raw(index);
  15. if raw < self.capacity() {
  16. unsafe {
  17. // We just checked `raw` against self.capacity(),
  18. // and index_to_raw skips the gap, so this is safe.
  19. Some(&*self.space(raw))
  20. }
  21. } else {
  22. None
  23. }
  24. }

当我们开始在缓冲区的不同部分进行插入和删除时,我们需要将间隙移动到新位置.向右移动间隙需要将元素向左移动,反之亦然,就像当流体在另一个方向上流动时,水平面中的气泡向一个方向移动:

  1. /// Set the current insertion position to `pos`.
  2. /// If `pos` is out of bounds, panic.
  3. pub fn set_position(&mut self, pos: usize) {
  4. if pos > self.len() {
  5. panic!("index {} out of range for GapBuffer", pos);
  6. }
  7. unsafe {
  8. let gap = self.gap.clone();
  9. if pos > gap.start {
  10. // `pos` falls after the gap. Move the gap right
  11. // by shifting elements after the gap to before it.
  12. let distance = pos - gap.start;
  13. std::ptr::copy(self.space(gap.end),
  14. self.space_mut(gap.start),
  15. distance);
  16. } else if pos < gap.start {
  17. // `pos` falls before the gap. Move the gap left
  18. // by shifting elements before the gap to after it.
  19. let distance = gap.start - pos;
  20. std::ptr::copy(self.space(pos),
  21. self.space_mut(gap.end - distance),
  22. distance);
  23. }
  24. self.gap = pos .. pos + gap.len();
  25. }
  26. }

该函数使用std::ptr::copy方法移动元素;copy要求目标未初始化,并使源未初始化.源和目标范围可能重叠,但copy正确地处理该情况.由于在调用之前间隙是未初始化的内存,并且函数调整间隙的位置以覆盖副本腾出的空间,因此满足copy函数的合同.

元素插入和移除相对简单.插入为新元素占据间隙的一个空间.而移除将一个值移出,并扩大间隙以覆盖它曾经占用的空间:

  1. /// Insert `elt` at the current insertion position,
  2. /// and leave the insertion position after it.
  3. pub fn insert(&mut self, elt: T) {
  4. if self.gap.len() == 0 {
  5. self.enlarge_gap();
  6. }
  7. unsafe {
  8. let index = self.gap.start;
  9. std::ptr::write(self.space_mut(index), elt);
  10. }
  11. self.gap.start += 1;
  12. }
  13. /// Insert the elements produced by `iter` at the current insertion
  14. /// position, and leave the insertion position after them.
  15. pub fn insert_iter<I>(&mut self, iterable: I)
  16. where I: IntoIterator<Item=T>
  17. {
  18. for item in iterable {
  19. self.insert(item)
  20. }
  21. }
  22. /// Remove the element just after the insertion position
  23. /// and return it, or return `None` if the insertion position
  24. /// is at the end of the GapBuffer.
  25. pub fn remove(&mut self) -> Option<T> {
  26. if self.gap.end == self.capacity() {
  27. return None;
  28. }
  29. let element = unsafe {
  30. std::ptr::read(self.space(self.gap.end))
  31. };
  32. self.gap.end += 1;
  33. Some(element)
  34. }

类似于Vec使用std::ptr::write用于推入(push)和std::ptr::read用于弹出(pop)的方式,GapBuffer使用write来插入,read来移除remove.正如Vec必须调整其长度以保持初始化的元素和备用容量之间的边界一样,GapBuffer会调整其间隙.

填充间隙后,insert方法必须增加缓冲区以获得更多可用空间.enlarge_gap方法(impl块中的最后一个)处理这个:

  1. /// Double the capacity of `self.storage`.
  2. fn enlarge_gap(&mut self) {
  3. let mut new_capacity = self.capacity() * 2;
  4. if new_capacity == 0 {
  5. // The existing vector is empty.
  6. // Choose a reasonable starting capacity.
  7. new_capacity = 4;
  8. }
  9. // We have no idea what resizing a Vec does with its "unused"
  10. // capacity. So just create a new vector and move over the elements.
  11. let mut new = Vec::with_capacity(new_capacity);
  12. let after_gap = self.capacity() - self.gap.end;
  13. let new_gap = self.gap.start .. new.capacity() - after_gap;
  14. unsafe {
  15. // Move the elements that fall before the gap.
  16. std::ptr::copy_nonoverlapping(self.space(0),
  17. new.as_mut_ptr(),
  18. self.gap.start);
  19. // Move the elements that fall after the gap.
  20. let new_gap_end = new.as_mut_ptr().offset(new_gap.end as isize);
  21. std::ptr::copy_nonoverlapping(self.space(self.gap.end),
  22. new_gap_end,
  23. after_gap);
  24. }
  25. // This frees the old Vec, but drops no elements,
  26. // because the Vec's length is zero.
  27. self.storage = new;
  28. self.gap = new_gap;
  29. }

set_position必须使用copy来在间隙中来回移动元素,而enlarge_gap可以使用copy_nonoverlapping,因为它将元素移动到一个新的缓冲区.

将新向量移动到self.storage会删除旧向量.由于它的长度为零,因此旧向量认为它没有要删除的元素,只是释放它的缓冲区.很巧妙地,copy_nonoverlapping保留了源为未初始化,因此旧的向量是正确的:所有元素现在都由新向量拥有.

最后,我们需要确保删除GapBuffer会删除其所有元素:

  1. impl<T> Drop for GapBuffer<T> {
  2. fn drop(&mut self) {
  3. unsafe {
  4. for i in 0 .. self.gap.start {
  5. std::ptr::drop_in_place(self.space_mut(i));
  6. }
  7. for i in self.gap.end .. self.capacity() {
  8. std::ptr::drop_in_place(self.space_mut(i));
  9. }
  10. }
  11. }
  12. }

元素位于间隙之前和之后,因此我们遍历每个区域并使用std::ptr::drop_in_place函数删除每个区域.drop_in_place函数是一个行为类似于drop(std :: ptr :: read(ptr))的工具,但不打扰将值移动到其调用者(因此适用于无大小的类型).就像在enlarge_gap中一样,当向量self.storage被删除时,它的缓冲区确实是未初始化的.

与我们在本章中展示的其他类型一样,GapBuffer确保其自身的不变量足以确保遵循其使用的每个不安全功能的合同,因此其公有方法都不需要标记为不安全.GapBuffer为无法在安全代码中有效编写的功能实现安全接口.

不安全代码中的恐慌安全(Panic Safety in Unsafe Code)

在Rust中,恐慌通常不会导致未定义行为;panic!宏不是一个不安全功能.但是,当你决定使用不安全的代码时,恐慌安全就会成为你工作的一部分.

考虑上一节中的GapBuffer::remove方法:

  1. pub fn remove(&mut self) -> Option<T> {
  2. if self.gap.end == self.capacity() {
  3. return None;
  4. }
  5. let element = unsafe {
  6. std::ptr::read(self.space(self.gap.end))
  7. };
  8. self.gap.end += 1;
  9. Some(element)
  10. }

调用read会在间隙离开缓冲区之后立即移动元素,留下未初始化的空间.幸运的是,下一个语句扩大了间隙以覆盖该空间,所以当我们返回时,一切都应该是这样:间隙之外的所有元素都被初始化,并且间隙内的所有元素都是未初始化的.

但是考虑一下,如果在调用read之后但在调整self.gap.end之前,这段代码试图使用可能会出现恐慌的功能—比如索引切片.这会发生什么.在这两个动作之间的任何地方突然退出该方法将使GapBuffer在间隙之外留下未初始化的元素.下一次remove调用可能会尝试再次read它;甚至只是删除GapBuffer就会试图删除它.两者都是未定义的行为,因为它们访问未初始化的内存.

对于类型的方法来说,在他们完成工作的同时暂时放松类型的不变量,然后在返回之前将所有内容恢复到正确,这几乎是不可避免的.恐慌的中间方法可以缩短清理过程,使类型处于不一致状态.

如果类型仅使用安全代码,则此不一致可能会使类型行为异常,但不会引入未定义行为.但是使用不安全功能的代码通常依靠其不变量来满足这些功能的合同.破坏不变量导致合同破环,从而导致未定义行为.

使用不安全功能时,你必须特别注意识别这些敏感区域,并确保它们不会做任何可能引起恐慌的事情.

外部函数:从Rust调用C和C++(Foreign Functions: Calling C and C++ from Rust)

Rust的 外部函数接口(foreign function interface) 允许Rust代码调用用C或C++编写的函数.

在本节中,我们将编写一个与libgit2链接的程序,libgit2是一个用于处理Git版本控制系统的C库.首先,我们将展示直接从Rust使用C函数的情况.然后,我们将展示如何构建一个安全的libgit2接口,从开源git2-rscrate中获取灵感.

我们假设你熟悉C以及编译和链接C程序的机制.使用C++是类似的.我们还假设你对Git版本控制系统有点熟悉.

寻找通用数据表示(Finding Common Data Representations)

Rust和C的共同点是机器语言,因此为了预测Rust值对C代码的影响,反之亦然,你需要考虑它们的机器级表示.在整本书中,我们已经明确了在内存中值实际如何表示,因此你可能已经注意到C和Rust的数据世界有很多共同之处:Rustusize和Csize_t是相同的,例如,结构在两种语言中基本上是相同的.为了建立Rust和C类型之间的对应关系,我们将从原语开始,然后逐步完成更复杂的类型.

鉴于其主要用作系统编程语言,C对其类型的表示总是令人惊讶地松散:int通常是32位长,但可能更长,或者短至16位;Cchar可以有符号的或无符号的;等等.为了应对这种可变性,Rust的std::os::raw模块定义了一组Rust类型,这些类型保证与某些C类型具有相同的表示形式.这些包括原始整数和字符类型:

C类型 相应的std::os::raw类型
short c_short
int c_int
long c_long
long long c_longlong
unsigned short c_ushort
unsigned,unsigned int c_uint c_uint
unsigned long c_ulong
unsigned long long c_ulonglong
char c_char
signed char c_schar
unsigned char c_uchar
float c_float
double c_double
void *,const void * *mut c_void,*const c_void

关于该表的一些注意事项:

  • 除了c_void之外,这里的所有Rust类型都是某些原始Rust类型的别名:例如,c_chari8u8.

  • 没有与C的bool对应的确定的Rust类型.目前,Rustbool始终为零或一个字节,与所有主要C和C++实现使用的表示相同.但是,Rust语言团队未承诺在将来保留此表示形式,因为这样做可能会关闭优化机会.

  • Rust的32位char类型不是wchar_t的类似物,其宽度和编码因实现而异.C的char32_t类型更接近,但其编码仍然不能保证是Unicode.

  • Rust的原始usizeisize类型与C的size_tptrdiff_t具有相同的表示形式.

  • C和C++指针和C++引用对应于Rust的原始指针类型,*mut T*const T.

  • 从技术上讲,C标准允许实现使用Rust没有相应类型的表示:36位整数,有符号值的符号数值表示法( sign-and-magnitude representations),等等.在实践中,在Rust已移植到的每个平台上,每个常见的C整数类型都有一个匹配Rust类型,bool除外.

要定义与C结构兼容的Rust结构类型,可以使用#[repr(C)]属性.将#[repr(C)]置于结构定义之上会要求Rust在内存中布局结构的字段,和C编译器布局类似的C结构类型一样.例如,libgit2git2/errors.h 头文件定义了以下C结构,以提供有关先前报告的错误的详细信息:

  1. typedef struct {
  2. char *message;
  3. int klass;
  4. } git_error;

你可以使用相同的表示定义Rust类型,如下所示:

  1. #[repr(C)]
  2. pub struct git_error {
  3. pub message: *const c_char,
  4. pub klass: c_int
  5. }

#[repr(C)]属性仅影响结构本身的布局,而不影响其各个字段的表示,因此为了匹配C结构,每个字段也必须使用类似C的类型:*const c_char用于char *,和c_int用于int,依此类推.

在这种特殊情况下,#[repr(C)]属性可能不会更改git_error的布局.确实没有太多有趣的方法来布置指针和整数.但是,C和C++保证结构的成员按照它们被声明的顺序出现在内存中,每个成员都在不同的地址,Rust重新排序字段以最小化结构的整体大小,而零大小的类型不占用空间.#[repr(C)]属性告诉Rust遵循给定类型的C规则.

你还可以使用#[repr(C)]来控制C风格枚举的表示形式:

  1. #[repr(C)]
  2. enum git_error_code {
  3. GIT_OK = 0,
  4. GIT_ERROR = -1,
  5. GIT_ENOTFOUND = -3,
  6. GIT_EEXISTS = -4,
  7. ...
  8. }

通常,Rust在选择如何表示枚举时会播放各种游戏.例如,我们提到了Rust用来在单个字中存储Option<&T>的技巧(如果T是有大小的).如果没有#[repr(C)],Rust将使用单个字节来表示git_error_code枚举;使用#[repr(C)],Rust使用一个Cint大小的值,就像C一样.

你还可以要求Rust为枚举提供与某种整数类型相同的表示形式.使用#[repr(i16)]开始前面的定义将为你提供一个16位类型,其表示形式与以下C++枚举相同:

  1. #include <stdint.h>
  2. enum git_error_code: int16_t {
  3. GIT_OK = 0,
  4. GIT_ERROR = -1,
  5. GIT_ENOTFOUND = -3,
  6. GIT_EEXISTS = -4,
  7. ...
  8. };

在Rust和C之间传递字符串有点困难.C表示字符串作为指向字符数组的指针,以空(null)字符结尾.另一方面,Rust显式地存储字符串的长度,可以是String的字段,也可以是胖引用&str的第二个字.Rust字符串不以null终止;实际上,它们可能在其内容中包含空(null)字符,就像任何其他字符一样.

这意味着你不能将Rust字符串借用为C字符串:如果将指向Rust字符串的指针传递给C代码,它可能会将嵌入的空字符误认为字符串的结尾,或者运行结束时查找不存在的终止null.另一个方面,只要其内容是格式良好的UTF-8,你就可以借用C字符串作为Rust&str.

这种情况有效地迫使Rust将C字符串视为完全不同于String&str的类型.在std::ffi模块中,CStringCStr类型表示拥有的和借用的以null终止的字节数组.与Stringstr相比,CStringCStr上的方法非常有限,仅限于构造和转换为其他类型.我们将在下一节中展示这些类型.

声明外部函数和变量(Declaring Foreign Functions and Variables)

extern块声明在一些其他库中定义的函数或变量,最终的Rust可执行文件将与之链接.例如,每个Rust程序都链接到标准C库,因此我们可以告诉Rust关于C库的strlen函数,如下所示:

  1. use std::os::raw::c_char;
  2. extern {
  3. fn strlen(s: *const c_char) -> usize;
  4. }

这为Rust提供了函数的名称和类型,同时保留了稍后要链接的定义.

Rust假定在extern块中声明的函数使用C约定来传递参数和接受返回值.它们被定义为unsafe函数.这对strlen来说是正确选择:它确实是一个C函数;并且它在C中的规范要求你将一个指向正确终止的字符串的有效指针传递给它,这是Rust无法强制执行的合同.(几乎所有接受原始指针的函数都必须是unsafe:安全的Rust可以从任意整数构造原始指针,解引用这样的指针将是未定义行为.)

有了这个extern块,我们可以像任何其他Rust函数一样调用strlen,尽管它的类型可以作为游客使用它:

  1. use std::ffi::CString;
  2. let rust_str = "I'll be back";
  3. let null_terminated = CString::new(rust_str).unwrap();
  4. unsafe {
  5. assert_eq!(strlen(null_terminated.as_ptr()), 12);
  6. }

CString::new函数构建一个以null终止的C字符串.它首先检查其嵌入空字符的参数,因为它们不能在C字符串中表示,并且如果找到任何字符则返回错误(因此需要unwrap结果).否则,它会在结尾添加一个空字节,并返回一个拥有结果字符的CString.

CString::new的成本取决于你传递的类型.它接受任何实现Into<Vec<u8>>的东西.传递&str需要分配和副本,因为转换为Vec<u8>会为要拥有的向量构建一个堆分配的字符串副本.但是通过值传递String只是消耗字符串并接管其缓冲区,因此除非附加空字符强制缓冲区调整大小,否则转换根本不需要复制文本或分配.

你还可以在extern块中声明全局变量.POSIX系统有一个名为environ的全局变量,它保存进程环境变量的值.在C中,它声明了:

  1. extern char **environ;

在Rust中,你可以说:

  1. use std::ffi::CStr;
  2. use std::os::raw::c_char;
  3. extern {
  4. static environ: *mut *mut c_char;
  5. }

要打印环境的第一个元素,你可以编写:

  1. unsafe {
  2. if !environ.is_null() && !(*environ).is_null() {
  3. let var = CStr::from_ptr(*environ);
  4. println!("first environment variable: {}",
  5. var.to_string_lossy())
  6. }
  7. }

在确保environ具有第一个元素之后,代码调用CStr::from_ptr来构建一个借用它的CStr.to_string_lossy方法返回一个Cow<str>:如果C字符串包含格式良好的UTF-8,则Cow将其内容借用为&str,不包括终止null字节.否则,to_string_lossy会在堆中创建文本的副本,用官方的Unicode替换字符'�''替换格式错误的UTF-8序列,并从中构建一个拥有的Cow.无论哪种方式,结果都实现了Display,因此你可以使用{}格式化参数进行打印.

使用库中的函数(Using Functions from Libraries)

要使用特定库提供的函数,可以在extern块的顶部放置一个#[link]属性,命名Rust应该将可执行文件链接到的库.例如,这是一个调用libgit2的初始化和关闭方法的程序,但不执行任何其他操作:

  1. use std::os::raw::c_int;
  2. #[link(name = "git2")]
  3. extern {
  4. pub fn git_libgit2_init() -> c_int;
  5. pub fn git_libgit2_shutdown() -> c_int;
  6. }
  7. fn main() {
  8. unsafe {
  9. git_libgit2_init();
  10. git_libgit2_shutdown();
  11. }
  12. }

extern块像以前一样声明外部函数.#[link(name ="git2")]属性在crate中留下一个注释,当Rust创建最终的可执行文件或共享库时,它应链接到git2库.Rust使用系统链接器来构建可执行文件;在Unix上,它在链接器命令行上传递参数-lgit2;在Windows上,它传递git2.LIB.

#[link]属性也适用于库crates.当你构建依赖于其他crates的程序时,Cargo会从整个依赖关系图中收集链接注释,并将它们全部包含在最终链接中.

在这个例子中,如果你想在自己的机器上运行,你需要自己构建libgit2.我们使用了libgit2版本0.25.1,可从 https://libgit2.github.com 获得.要编译libgit2,你需要安装CMake构建工具和Python语言;我们使用了CMake版本3.8.0和Python版本2.7.13,从 https://cmake.orghttps://www.python.org 下载.

有关构建libgit2的完整说明可在其网站上找到,但它们非常简单,我们将在此处显示基本要素.在Linux上,假设你已经将库的源解压缩到目录 /home/jimb/libgit2-0.25.1 中:

  1. $ cd /home/jimb/libgit2-0.25.1
  2. $ mkdir build
  3. $ cd build
  4. $ cmake ..
  5. $ cmake --build .

在Linux上,这会生成一个共享库 /home/jimb/libgit2-0.25.1/build/libgit2.so.0.25.1 ,通常的符号链接嵌套指向它,包括一个名为 libgit2.so 的符号链接.在macOS上,结果类似,但该库名为 libgit2.dylib .

在Windows上,事情也很简单.假设你已将源解压缩到目录 C:\Users\JimB\libgit2-0.25.1 中.在Visual Studio命令提示符中:

  1. > cd C:\Users\JimB\libgit2-0.25.1
  2. > mkdir build
  3. > cd build
  4. > cmake -A x64 ..
  5. > cmake --build .

这些命令与Linux上使用的命令相同,只是在第一次运行CMake时必须要求64位构建,以匹配Rust编译器.(如果已安装32位Rust工具链,则应省略第一个cmake命令的-A x64标志.)这将在目录中生成导入库 git2.LIB 和动态链接库 git2.DLL ,同时在目录 C:\Users\JimB\libgit2-0.25.1\build\Debug .(其余的说明是针对Unix显示的,除非Windows有很大差异.)

在单独的目录中创建Rust程序:

  1. $ cd /home/jimb
  2. $ cargo new --bin git-toy

将上面的代码放在 src/main.rs 中.当然,如果你尝试构建它,Rust不知道在哪里可以找到你构建的libgit2:

  1. $ cd git-toy
  2. $ cargo run
  3. Compiling git-toy v0.1.0 (file:///home/jimb/git-toy)
  4. error: linking with `cc` failed: exit code: 1
  5. |
  6. = note: "cc" ... "-l" "git2" ...
  7. = note: /usr/bin/ld: cannot find -lgit2
  8. collect2: error: ld returned 1 exit status
  9. error: aborting due to previous error
  10. error: Could not compile `git-toy`.
  11. To learn more, run the command again with --verbose.
  12. $

你可以通过编写 构建脚本(build script) 来告诉Rust在哪里搜索库,即Cargo编译并在构建时运行的Rust代码.构建脚本可以做各种事情:动态生成代码,编译C代码,包含在crate中,等等.在这种情况下,你只需要在可执行文件的链接命令中添加库搜索路径.当Cargo运行构建脚本时,它会解析构建脚本的输出以获取此类信息,因此构建脚本只需要将正确的魔法打印到其标准输出.

要创建构建脚本,请在 Cargo.toml 文件所在的目录中添加名为 build.rs 的文件,其中包含以下内容:

  1. fn main() {
  2. println!(r"cargo:rustc-link-search=native=/home/jimb/libgit2-0.25.1/build");
  3. }

这是Linux的正确途径;在Windows上,你可以将文本native =之后的路径更改为C:\Users\JimB\libgit2-0.25.1\build\Debug(为了简化这个例子,我们做了一些偷工减料的工作;在实际应用程序中,你应该避免在构建脚本中使用绝对路径.我们在本节末尾引用了说明如何正确执行该操作的文档.)

接下来,通过将行build = "build.rs"添加到 Cargo.toml 文件的[package]部分,告诉Cargo这是你的构建脚本.整个文件现在应该是:

  1. [package]
  2. name = "git-toy"
  3. version = "0.1.0"
  4. authors = ["You <you@example.com>"]
  5. build = "build.rs"
  6. [dependencies]

现在你几乎可以运行该程序了.在macOS上它可以立即工作;在Linux系统上,你可能会看到如下内容:

  1. $ cargo run
  2. Compiling git-toy v0.1.0 (file:///home/jimb/git-toy)
  3. Finished dev [unoptimized + debuginfo] target(s) in 0.64 secs
  4. Running `target/debug/git-toy`
  5. target/debug/git-toy: error while loading shared libraries:
  6. libgit2.so.25: cannot open shared object file: No such file or directory
  7. $

这意味着,虽然Cargo成功地将可执行文件链接到库,但它不知道在运行时在何处查找共享库.Windows通过弹出对话框报告此故障.在Linux上,你必须设置LD_LIBRARY_PATH环境变量:

  1. $ export LD_LIBRARY_PATH=/home/jimb/libgit2-0.25.1/build:$LD_LIBRARY_PATH
  2. $ cargo run
  3. Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
  4. Running `target/debug/git-toy`
  5. $

在macOS上,你可能需要设置DYLD_LIBRARY_PATH.

在Windows上,你必须设置PATH环境变量:

  1. > set PATH=C:\Users\JimB\libgit2-0.25.1\build\Debug;%PATH%
  2. > cargo run
  3. Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
  4. Running `target/debug/git-toy`
  5. >

当然,在已部署的应用程序中,你需要避免仅为查找库的代码而设置环境变量.另一种方法是将C库静态链接到你的crate中.这会将库的目标文件复制到crate的 .rlib 文件中,以及crate的Rust代码的目标文件和元数据.然后整个集合参与最后的链接.

Cargo约定,提供对C库的访问的crate应该命名为LIB-sys,其中LIB是C库的名称.-syscrate应该只包含静态链接库和包含extern块和类型定义的Rust模块.然后,更高级别的接口属于依赖于-sys包的crate.这允许多个上游crates依赖于相同的-syscrate,假设有一个单个版本的-syscrate满足每个人的需求.

有关Cargo支持构建脚本和链接系统库的完整详细信息,请参阅在线Cargo文档.它显示了如何避免构建脚本中的绝对路径,控制编译标志,使用pkg-config等工具,等等.git2-rs包也提供了很好的例子来模拟;它的构建脚本处理一些复杂的情况.

libgit2的原始接口(A Raw Interface to libgit2)

弄清楚如何正确使用libgit2分为两个问题:

  • 在Rust中使用libgit2函数需要什么?

  • 我们如何围绕它们构建安全的Rust接口?

我们将同时解决这些问题.在本节中,我们将编写一个程序,它本质上是一个充满非惯用法的Rust代码的巨大unsafe块,反映了混合语言中固有的类型系统和约定的冲突.我们称之为 原始(raw) 接口.代码将是混乱的,但它将使Rust代码使用libgit2必须发生的所有步骤都清楚.

然后,在下一节中,我们将构建一个安全的libgit2接口,它将Rust的类型用于强制执行libgit2对其用户施加的规则.幸运的是,libgit2是一个设计得非常好的C库,因此Rust的安全要求迫使我们提出的问题都有很好的答案,我们可以构建一个没有unsafe函数的惯用Rust接口.

我们要编写的程序非常简单:它将路径作为命令行参数,在那里打开Git存储库,并打印出head commit.但这足以说明构建安全和惯用Rust接口的关键策略.

对于原始接口,程序最终需要从libgit2获得比我们之前使用的更大的函数和类型集合,因此将extern块移动到其自己的模块中是有意义的.我们将在git-toy/src中创建一个名为raw.rs的文件,其内容如下:

  1. #![allow(non_camel_case_types)]
  2. use std::os::raw::{c_int, c_char, c_uchar};
  3. #[link(name = "git2")]
  4. extern {
  5. pub fn git_libgit2_init() -> c_int;
  6. pub fn git_libgit2_shutdown() -> c_int;
  7. pub fn giterr_last() -> *const git_error;
  8. pub fn git_repository_open(out: *mut *mut git_repository,
  9. path: *const c_char) -> c_int;
  10. pub fn git_repository_free(repo: *mut git_repository);
  11. pub fn git_reference_name_to_id(out: *mut git_oid,
  12. repo: *mut git_repository,
  13. reference: *const c_char) -> c_int;
  14. pub fn git_commit_lookup(out: *mut *mut git_commit,
  15. repo: *mut git_repository,
  16. id: *const git_oid) -> c_int;
  17. pub fn git_commit_author(commit: *const git_commit) -> *const git_signature;
  18. pub fn git_commit_message(commit: *const git_commit) -> *const c_char;
  19. pub fn git_commit_free(commit: *mut git_commit);
  20. }
  21. pub enum git_repository {}
  22. pub enum git_commit {}
  23. #[repr(C)]
  24. pub struct git_error {
  25. pub message: *const c_char,
  26. pub klass: c_int}
  27. #[repr(C)]
  28. pub struct git_oid {
  29. pub id: [c_uchar; 20]
  30. }
  31. pub type git_time_t = i64;
  32. #[repr(C)]
  33. pub struct git_time {
  34. pub time: git_time_t,
  35. pub offset: c_int
  36. }
  37. #[repr(C)]
  38. pub struct git_signature {
  39. pub name: *const c_char,
  40. pub email: *const c_char,
  41. pub when: git_time
  42. }

这里的每一项都是根据libgit2自己的头文件的声明建模的.例如, libgit2-0.25.1/include/git2/repository.h 包含以下声明:

  1. extern int git_repository_open(git_repository **out, const char *path);

此函数尝试在path打开Git存储库.如果一切顺利,它会创建一个git_repository对象,并在out指向的位置存储指向它的指针.等效的Rust声明如下:

  1. pub fn git_repository_open(out:*mut *mut git_repository,
  2. path:*const c_char) -> c_int;

libgit2公有头文件将git_repository类型定义为不完整结构类型的typedef:

  1. typedef struct git_repository git_repository;

由于此类型的详细信息对库是私有的,因此公有头文件永远不会定义struct git_repository,从而确保库的用户永远不会自己构建此类型的实例.Rust中不完整的结构类型的一个可能类似于:

  1. pub enum git_repository {}

这是一个没有变体的枚举类型.Rust中没有办法制作这种类型的值.这是一个奇怪的问题,但它完美地反映了只有libgit2应该构造的C类型,并且只能通过原始指针进行操作.

手工编写大型extern块可能是件苦差事.如果要为复杂的C库创建Rust接口,则可能需要尝试使用bindgencrate,它具有可以从构建脚本中使用的函数来解析C头文件并自动生成相应的Rust声明.我们没有空间在这里显示bindgen,但是crate.io上的bindgen页面包含其文档的链接.

接下来我们将完全重写 main.rs .首先,我们需要声明raw模块:

  1. mod raw;

根据libgit2的约定,易错函数返回一个整数代码,该代码在成功时为正或零,在失败时为负.如果发生错误,giterr_last函数将返回指向git_error结构的指针,它提供有关错误的更多详细信息.libgit2拥有这个结构,所以我们不需要自己释放它,但它可以被我们下一个库调用覆盖.一个正确的Rust接口会使用Result,但在原始版本中,我们希望使用libgit2函数就如它们本来的样子,因此我们必须使用自己的函数来处理错误:

  1. use std::ffi::CStr;
  2. use std::os::raw::c_int;
  3. fn check(activity: &'static str, status: c_int) -> c_int {
  4. if status < 0 {
  5. unsafe {
  6. let error = &*raw::giterr_last();
  7. println!("error while {}: {} ({})",
  8. activity,
  9. CStr::from_ptr(error.message).to_string_lossy(),
  10. error.klass);
  11. std::process::exit(1);
  12. }
  13. }
  14. status
  15. }

我们将使用此函数来检查libgit2调用的结果,如下所示:

  1. check("initializing library", raw::git_libgit2_init());

这使用了和前面使用的相同的CStr方法:from_ptr从C字符串构造CStr,而to_string_lossy将其转换为Rust可以打印的东西.

接下来,我们需要一个函数来打印commit:

  1. unsafe fn show_commit(commit: *const raw::git_commit) {
  2. let author = raw::git_commit_author(commit);
  3. let name = CStr::from_ptr((*author).name).to_string_lossy();
  4. let email = CStr::from_ptr((*author).email).to_string_lossy();
  5. println!("{} <{}>\n", name, email);
  6. let message = raw::git_commit_message(commit);
  7. println!("{}", CStr::from_ptr(message).to_string_lossy());
  8. }

给定指向git_commit的指针,show_commit调用git_commit_authorgit_commit_message来检索它需要的信息.这两个函数遵循libgit2文档解释如下的约定:

如果函数返回一个对象作为返回值,则该函数是一个getter,并且该对象的生命周期与父对象相关联.

在Rust术语中,authormessage是从commit中借来的:show_commit不需要自己释放它们,但是在释放commit后它不能保留它们.由于这个API使用原始指针,Rust不会为我们检查它们的生命周期:如果我们不小心创建了悬空指针,我们可能会在程序崩溃之前找不到它.

前面的代码假定这些字段包含UTF-8文本,这并不总是正确的.Git也允许其他编码.正确解释这些字符串可能需要使用encodingcrate.为了简洁起见,我们将在这里掩饰这些问题.

我们程序的main函数如下:

  1. use std::ffi::CString;
  2. use std::mem;
  3. use std::ptr;
  4. use std::os::raw::c_char;
  5. fn main() {
  6. let path = std::env::args().skip(1).next()
  7. .expect("usage: git-toy PATH");
  8. let path = CString::new(path)
  9. .expect("path contains null characters");
  10. unsafe {
  11. check("initializing library", raw::git_libgit2_init());
  12. let mut repo = ptr::null_mut();
  13. check("opening repository",
  14. raw::git_repository_open(&mut repo, path.as_ptr()));
  15. let c_name = b"HEAD\0".as_ptr() as *const c_char;
  16. let mut oid = mem::uninitialized();
  17. check("looking up HEAD",
  18. raw::git_reference_name_to_id(&mut oid, repo, c_name));
  19. let mut commit = ptr::null_mut();
  20. check("looking up commit",
  21. raw::git_commit_lookup(&mut commit, repo, &oid));
  22. show_commit(commit);
  23. raw::git_commit_free(commit);
  24. raw::git_repository_free(repo);
  25. check("shutting down library", raw::git_libgit2_shutdown());
  26. }
  27. }

这开始于处理路径参数和初始化库的代码,我们之前已经看到了所有这些.第一个新颖的代码是这样的:

  1. let mut repo = ptr::null_mut();
  2. check("opening repository",
  3. raw::git_repository_open(&mut repo, path.as_ptr()));

git_repository_open的调用尝试在给定路径上打开Git存储库.如果成功,则为其分配新的git_repository对象,并将repo设置为指向该对象.Rust隐式地强制引用到原始指针,因此传递&mut repo在这里提供了调用期望的* mut *mut git_repository.

这显示了另一个正在使用的libgit2约定.再次,来自libgit2文档:

通过第一个参数作为指向指针的指针返回的对象由调用者拥有,它负责释放它们.

在Rust术语中,git_repository_open等函数将新值的所有权传递给调用者.

接下来,考虑查找存储库当前head commit的对象哈希码:

  1. let mut oid = mem::uninitialized();
  2. check("looking up HEAD",
  3. raw::git_reference_name_to_id(&mut oid, repo, c_name));

git_oid类型存储一个对象标识符—Git在内部(以及整个令人愉快的用户接口)使用的160位哈希码,用于标识提交,文件的各个版本等.对git_reference_name_to_id的调用会查找当前"HEAD"commit的对象标识符.

在C中,通过将指针传递给某个填充其值的函数来初始化变量是完全正常的.这就是git_reference_name_to_id期望处理其第一个参数的方式.但Rust不会让我们借用对未初始化变量的引用.我们可以用零初始化oid,但这是一种浪费:存储在那里的任何值都将被覆盖.

oid初始化为uninitialized()可以解决这个问题.std::mem::uninitialized函数返回你喜欢的任何类型的值,只是该值完全由未初始化的位组成,并且实际上没有机器代码用于生成该值.但是,Rust认为oid被分配了一些值,所以它允许我们借用它的引用.可以想象,在一般情况下,这是非常不安全的.读取未初始化的值是未定义行为,如果值的任何部分实现Drop,甚至删除它也是未定义行为.你可以做的只有一些安全的事情:

  • 你可以使用std::ptr::write覆盖它,这需要其目标成为未初始化.

  • 你可以将它传递给std::mem::forget,它将获取其参数的所有权并使其消失而不删除它(将此应用于初始值可能是泄漏).

  • 你可以将它传递给一个旨在初始化它的外部函数,比如git_reference_name_to_id.

如果调用成功,那么oid会真正初始化,一切都很好.如果调用失败,该函数不使用oid,并且不需要删除它的类型,因此代码在这种情况下也是安全的.

我们也可以将uninitialized用于repocommit变量,但由于这些只是单个字而uninitialized使用起来如此冒险,我们只需将它们初始化为null:

  1. let mut commit = ptr::null_mut();
  2. check("looking up commit",
  3. raw::git_commit_lookup(&mut commit, repo, &oid));

这将获取commit的对象标识符并查找实际commit,并在成功时在commit中存储git_commit指针.

main函数的其余部分应该是不言自明的.它调用前面定义的show_commit函数,释放commit和存储库对象,并关闭库.

现在我们可以在任何准备好的Git存储库上试用该程序:

  1. $ cargo run /home/jimb/rbattle
  2. Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
  3. Running `target/debug/git-toy /home/jimb/rbattle`
  4. Jim Blandy <jimb@red-bean.com>
  5. Animate goop a bit.
  6. $

libgit2的安全接口(A Safe Interface to libgit2)

libgit2的原始接口是一个不安全功能的完美示例:它当然可以正确使用(就像我们所做,就我们所知),但Rust无法强制执行你必须遵循的规则.为这样的库设计一个安全的API是一个识别所有这些规则的问题,然后找到将它们的任何违反都变成类型或借用检查错误的方法.

那么,这是libgit2对程序使用的功能的规则:

  • 在使用任何其他库函数之前,必须调用git_libgit2_init.调用git_libgit2_shutdown后,不得使用任何库函数.

  • 除输出参数外,必须完全初始化传递给libgit2函数的所有值.

  • 当调用失败时,传递的用于保存调用结果的输出参数将保持未初始化状态,你不能使用它们的值.

  • git_commit对象引用它派生自的git_repository对象,因此前者不得活得超过后者.(这在libgit2文档中没有说明;我们从接口中某些函数的存在推断出它,然后通过阅读源代码对其进行验证.)

  • 类似地,git_signature总是从给定的git_commit中借用,前者不能活得比后者长.(文档确实涵盖了这种情况.)

  • 与commit相关联的消息以及作者的名称和电子邮件地址都是从commit中借用的,并且在释放commit后不得使用.

  • 一旦释放了libgit2对象,就不能再使用它.

事实证明,你可以构建一个libgit2的Rust接口,通过Rust的类型系统或内部管理细节来强制执行所有这些规则.

在我们开始之前,让我们重新调整一下项目.我们想要一个导出安全接口的git模块,其中前一个程序的原始接口是私有子模块.

整个源代码树将如下所示:

  1. git-toy/
  2. ├── Cargo.toml
  3. ├── build.rs
  4. └── src/
  5. ├── main.rs
  6. └── git/
  7. ├── mod.rs
  8. └── raw.rs

遵循我们在第166页的”单独文件中的模块(Modules in Separate Files)”中解释的规则,git模块的源代码显示在 git/mod.rs 中,其git::raw子模块的源代码位于 git/raw.rs 中.

再一次,我们将完全重写 main.rs .它应该从git模块的声明开始:

  1. mod git;

然后,我们需要创建 git 子目录,并将 raw.rs 移动到其中:

  1. $ cd /home/jimb/git-toy
  2. $ mkdir src/git
  3. $ mv src/raw.rs src/git/raw.rs

git模块需要声明其raw子模块.文件 src/git/mod.rs 必须说:

  1. mod raw;

由于它不是pub,因此主程序看不到该子模块.

稍后我们需要使用libccrate中的一些函数,因此我们必须在 Cargo.toml 中添加一个依赖项.完整文件现在显示为:

  1. [package]
  2. name = "git-toy"
  3. version = "0.1.0"
  4. authors = ["Jim Blandy <jimb@red-bean.com>"]
  5. build = "build.rs"
  6. [dependencies]
  7. libc = "0.2.23"

相应的extern crate项必须出现在 src/main.rs 中:

  1. extern crate libc;

现在我们重新构建了模块,让我们考虑错误处理.即使libgit2的初始化函数也可以返回错误码,因此我们需要在开始之前对其进行整理.惯用的Rust接口需要自己的Error类型,它捕获libgit2失败码以及giterr_last中的错误消息和类.正确的错误类型必须实现通常的Error,DebugDisplaytrait.然后,它需要使用此Error类型的自己的Result类型.以下是 src/git/mod.rs 中的必要定义:

  1. use std::error;
  2. use std::fmt;
  3. use std::result;
  4. #[derive(Debug)]
  5. pub struct Error {
  6. code: i32,
  7. message: String,
  8. class: i32
  9. }
  10. impl fmt::Display for Error {
  11. fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
  12. // Displaying an `Error` simply displays the message from libgit2.
  13. self.message.fmt(f)
  14. }
  15. }
  16. impl error::Error for Error {
  17. fn description(&self) -> &str { &self.message }
  18. }
  19. pub type Result<T> = result::Result<T, Error>;

要检查原始库调用的结果,模块需要一个将libgit2返回代码转换为Result的函数:

  1. use std::os::raw::c_int;
  2. use std::ffi::CStr;
  3. fn check(code: c_int) -> Result<c_int> {
  4. if code >= 0 {
  5. return Ok(code);
  6. }
  7. unsafe {
  8. let error = raw::giterr_last();
  9. // libgit2 ensures that (*error).message is always non-null and null
  10. // terminated, so this call is safe.
  11. let message = CStr::from_ptr((*error).message)
  12. .to_string_lossy()
  13. .into_owned();
  14. Err(Error {
  15. code: code as i32,
  16. message,
  17. class: (*error).klass as i32
  18. })
  19. }
  20. }

这与原始版本的check函数之间的主要区别在于,它构造了一个Error值,而不是打印错误消息并立即退出.

现在我们已准备好解决libgit2初始化问题.安全接口将提供表示开放Git存储库的Repository类型,其中包含用于解析引用,查找commit等的方法.继续 git/mod.rs ,这里是Repository的定义:

  1. /// A Git repository.
  2. pub struct Repository {
  3. // This must always be a pointer to a live `git_repository` structure.
  4. // No other `Repository` may point to it.
  5. raw: *mut raw::git_repository
  6. }

Repositoryraw字段不公有.由于只有此模块中的代码可以访问raw::git_repository指针,因此正确使用此模块应确保指针始终正确使用.

如果创建Repository的唯一方法是成功打开一个新的Git存储库,那么将确保每个Repository指向一个不同的git_repository对象:

  1. use std::path::Path;
  2. impl Repository {
  3. pub fn open<P: AsRef<Path>>(path: P) -> Result<Repository> {
  4. ensure_initialized();
  5. let path = path_to_cstring(path.as_ref())?;
  6. let mut repo = null_mut();
  7. unsafe {
  8. check(raw::git_repository_open(&mut repo, path.as_ptr()))?;
  9. }
  10. Ok(Repository { raw: repo })
  11. }
  12. }

由于使用安全接口执行任何操作的唯一方法是从Repository值开始,并且Repository::open以调用ensure_initialized开始,因此我们可以确信在任何libgit2函数之前将调用ensure_initialized.其定义如下:

  1. use std;
  2. use libc;
  3. fn ensure_initialized() {
  4. static ONCE: std::sync::Once = std::sync::ONCE_INIT;
  5. ONCE.call_once(|| {
  6. unsafe {
  7. check(raw::git_libgit2_init())
  8. .expect("initializing libgit2 failed");
  9. assert_eq!(libc::atexit(shutdown), 0);
  10. }
  11. });
  12. }
  13. use std::io::Write;
  14. extern fn shutdown() {
  15. unsafe {
  16. if let Err(e) = check(raw::git_libgit2_shutdown()) {
  17. let _ = writeln!(std::io::stderr(),
  18. "shutting down libgit2 failed: {}",
  19. e);
  20. std::process::abort();
  21. }
  22. }
  23. }

std::sync::Once类型有助于以线程安全的方式运行初始化代码.只有第一个调用ONCE.call_once的线程才会运行给定的闭包.此线程或任何其他任何后续调用将阻塞,直到第一个完成,然后立即返回,而不再运行闭包.一旦闭包完成,调用ONCE.call_once很便宜,只需要存储在ONCE中的标志的原子加载.

在前面的代码中,初始化闭包调用git_libgit2_init并检查结果.它有点用,只是使用expect来确保初始化成功,而不是试图将错误传播回调用者.

为了确保程序调用git_libgit2_shutdown,初始化闭包使用C库的atexit函数,该函数在进程退出之前获取要调用的函数的指针.Rust闭包不能作为C函数指针:闭包是一些匿名类型的值,它携带它捕获的任何变量的值,或者引用它们;C函数指针只是一个指针.但是,Rustfn类型工作正常,只要你将它们声明为extern,以便Rust知道使用C调用约定.本地函数shutdown适合此情况,并确保libgit2正常关闭.

在第146页的”展开(Unwinding)”中,我们提到了跨语言边界的恐慌是未定义行为.从atexitshutdown的调用是这样的边界,因此shutdown不要惊慌是必要的.这就是为什么shutdown不能简单地使用.expect来处理raw::git_libgit2_shutdown报告的错误.相反,它必须报告错误并终止进程本身.POSIX禁止在atexit处理程序中调用exit,因此shutdown调用std::process::abort以突然终止程序.

有可能安排尽快调用git_libgit2_shutdown—比如当最后一个Repository值被删除时.但无论我们如何安排事情,调用git_libgit2_shutdown都必须是安全API的责任.它被调用的那一刻,任何现存的libgit2对象都变得不安全,因此安全的API不能直接暴露这个函数.

Repository的原始指针必须始终指向活着的git_repository对象.这意味着关闭存储库的唯一方法是删除拥有它的Repository值:

  1. impl Drop for Repository {
  2. fn drop(&mut self) {
  3. unsafe {
  4. raw::git_repository_free(self.raw);
  5. }
  6. }
  7. }

通过仅在指向raw::git_repository的唯一指针即将消失时调用git_repository_free,Repository类型还确保指针在释放后永远不会被使用.

Repository::open方法使用一个名为path_to_cstring的私有函数,它有两个定义—一个用于类Unix系统.一个用于Windows:

  1. use std::ffi::CString;
  2. #[cfg(unix)]
  3. fn path_to_cstring(path: &Path) -> Result<CString> {
  4. // The `as_bytes` method exists only on Unix-like systems.
  5. use std::os::unix::ffi::OsStrExt;
  6. Ok(CString::new(path.as_os_str().as_bytes())?)
  7. }
  8. #[cfg(windows)]
  9. fn path_to_cstring(path: &Path) -> Result<CString> {
  10. // Try to convert to UTF-8. If this fails, libgit2 can't handle the path
  11. // anyway.
  12. match path.to_str() {
  13. Some(s) => Ok(CString::new(s)?),
  14. None => {
  15. let message = format!("Couldn't convert path '{}' to UTF-8",
  16. path.display());
  17. Err(message.into())
  18. }
  19. }
  20. }

libgit2接口使这段代码有点棘手.在所有平台上,libgit2接受路径为以null终止的C字符串.在Windows上,libgit2假设这些C字符串保存格式良好的UTF-8,并在内部将它们转换为Windows实际需要的16位路径.这通常有效,但并不理想.Windows允许文件名不是格式良好的Unicode,因此无法用UTF-8表示.如果你有这样的文件,则无法将其名称传递给libgit2.

在Rust中,文件系统路径的正确表示是std::path::Path,经过精心设计,可以处理Windows或POSIX上可以出现的任何路径.这意味着Windows上存在无法传递给libgit2Path值,因为它们不是格式良好的UTF-8.因此虽然path_to_cstring的行为不太理想,但实际上我们可以在libgit2的接口上做到最好.

刚刚显示的两个path_to_cstring定义依赖于我们的Error类型的转换:?运算符尝试此类转换,Windows版本显式调用.into().这些转换不起眼:

  1. impl From<String> for Error {
  2. fn from(message: String) -> Error {
  3. Error { code: -1, message, class: 0 }
  4. }
  5. }
  6. // NulError is what `CString::new` returns if a string
  7. // has embedded zero bytes.
  8. impl From<std::ffi::NulError> for Error {
  9. fn from(e: std::ffi::NulError) -> Error {
  10. Error { code: -1, message: e.to_string(), class: 0 }
  11. }
  12. }

接下来,让我们弄清楚如何解析对象标识符的Git引用.由于对象标识符只是一个20字节的哈希值,因此在安全的API中公开它是完全正确的:

  1. /// The identifier of some sort of object stored in the Git object
  2. /// database: a commit, tree, blob, tag, etc. This is a wide hash of the
  3. /// object's contents.
  4. pub struct Oid {
  5. pub raw: raw::git_oid
  6. }

我们将向Repository添加一个方法来执行查找:

  1. use std::mem::uninitialized;
  2. use std::os::raw::c_char;
  3. impl Repository {
  4. pub fn reference_name_to_id(&self, name: &str) -> Result<Oid> {
  5. let name = CString::new(name)?;
  6. unsafe {
  7. let mut oid = uninitialized();
  8. check(raw::git_reference_name_to_id(&mut oid, self.raw,
  9. name.as_ptr() as *const c_char))?;
  10. Ok(Oid { raw: oid })
  11. }
  12. }
  13. }

虽然oid在查找失败时保持未初始化,但是这个函数通过遵循Rust的Result习惯用法来保证它的调用者不会看到未初始化的值:调用者得到一个带有正确初始化的Oid值的Ok,或者它得到一个Err.

接下来,该模块需要一种从存储库中检索commits的方法.我们将定义一个Commit类型,如下所示:

  1. use std::marker::PhantomData;
  2. pub struct Commit<'repo> {
  3. // This must always be a pointer to a usable `git_commit` structure.
  4. raw: *mut raw::git_commit,
  5. _marker: PhantomData<&'repo Repository>
  6. }

正如我们前面提到的,git_commit对象必须永远不会活得超过从中检索它的git_repository对象.Rust的生命周期让代码准确地捕获了这个规则.

本章前面的RefWithFlag示例使用PhantomData字段告诉Rust将类型视为包含具有给定生命周期的引用,即使该类型显然不包含此类引用.Commit类型需要做类似的事情.在这种情况下,_marker字段的类型是PhantomData<&'repo Repository>,表明Rust应该将Commit<'repo>视为保存带有生命周期'repo的对某个Repository的引用.

查找commits的方法如下:

  1. use std::ptr::null_mut;
  2. impl Repository {
  3. pub fn find_commit(&self, oid: &Oid) -> Result<Commit> {
  4. let mut commit = null_mut();
  5. unsafe {
  6. check(raw::git_commit_lookup(&mut commit, self.raw, &oid.raw))?;
  7. }
  8. Ok(Commit { raw: commit, _marker: PhantomData })
  9. }
  10. }

这如何将Commit的生命周期与Repository的生命周期联系起来?find_commit的签名根据第112页中”省略生命周期参数(Omitting Lifetime Parameters)”概述的规则省略了所涉及引用的生命周期.如果我们要写出生命周期,那么完整的签名将是:

  1. fn find_commit<'repo, 'id>(&'repo self, oid: &'id Oid)
  2. -> Result<Commit<'repo>>

这正是我们想要的:Rust将返回的Commit视为从self(即Repository)借用的内容.

当一个Commit被删除时,它必须释放它的raw::git_commit:

  1. impl<'repo> Drop for Commit<'repo> {
  2. fn drop(&mut self) {
  3. unsafe {
  4. raw::git_commit_free(self.raw);
  5. }
  6. }
  7. }

Commit种,你可以借用Signature(名称和电子邮件地址)和提交消息的文本:

  1. impl<'repo> Commit<'repo> {
  2. pub fn author(&self) -> Signature {
  3. unsafe {
  4. Signature {
  5. raw: raw::git_commit_author(self.raw),
  6. _marker: PhantomData
  7. }
  8. }
  9. }
  10. pub fn message(&self) -> Option<&str> {
  11. unsafe {
  12. let message = raw::git_commit_message(self.raw);
  13. char_ptr_to_str(self, message)
  14. }
  15. }
  16. }

这是Signature类型:

  1. pub struct Signature<'text> {
  2. raw: *const raw::git_signature,
  3. _marker: PhantomData<&'text str>
  4. }

git_signature对象总是从其他地方借用它的文本;特别是,git_commit_author返回的签名从git_commit中借用了它们的文本.所以我们的安全Signature类型包括一个PhantomData<&'text str>来告诉Rust表现得好像它包含一个带有'text'生命周期的&str.和以前一样,Commit::author正确地将它返回的Signature'text生命周期连接到Commit的,而不需要编写任何东西.Commit::message方法对包含提交消息的Option<&str>执行相同的操作.

Signature包含检索作者姓名和电子邮件地址的方法:

  1. impl<'text> Signature<'text> {
  2. /// Return the author's name as a `&str`,
  3. /// or `None` if it is not well-formed UTF-8.
  4. pub fn name(&self) -> Option<&str> {
  5. unsafe {
  6. char_ptr_to_str(self, (*self.raw).name)
  7. }
  8. }
  9. /// Return the author's email as a `&str`,
  10. /// or `None` if it is not well-formed UTF-8.
  11. pub fn email(&self) -> Option<&str> {
  12. unsafe {
  13. char_ptr_to_str(self, (*self.raw).email)
  14. }
  15. }
  16. }

上述方法依赖于私有工具函数char_ptr_to_str:

  1. /// Try to borrow a `&str` from `ptr`, given that `ptr` may be null or
  2. /// refer to ill-formed UTF-8. Give the result a lifetime as if it were
  3. /// borrowed from `_owner`.
  4. ///
  5. /// Safety: if `ptr` is non-null, it must point to a null-terminated C
  6. /// string that is safe to access.
  7. unsafe fn char_ptr_to_str<T>(_owner: &T, ptr: *const c_char) -> Option<&str> {
  8. if ptr.is_null() {
  9. return None;
  10. } else {
  11. CStr::from_ptr(ptr).to_str().ok()
  12. }
  13. }

_owner参数的值从未使用过,但它的生命周期就在那里.此函数的签名中的生命周期显式是这样的:

  1. fn char_ptr_to_str<'o, T: 'o>(_owner: &'o T, ptr: *const c_char)
  2. -> Option<&'o str>

CStr::from_ptr函数返回一个&CStr,它的生命周期是完全无界的,因为它是从一个解引用的原始指针借来的.无界的生命周期几乎总是不准确的,所以尽快约束它们是好的.包含_owner参数会导致Rust将其生命周期归因于返回值的类型,因此调用者可以接收更准确的有界引用.

libgit2文档中不清楚git_signatureemailauthor指针是否可以为null,尽管libgit2的文档非常好.你的作者在源代码中挖掘了一段时间而无法以某种方式说服自己,最后决定为了以防万一,char_ptr_to_str最好为空指针做好准备.在Rust中,这类问题会立即通过类型回答:如果是&str,你可以指望字符串在那里;如果它是Option<&str>,那么它是可选的.

最后,我们为所需的所有功能提供了安全的接口. src/main.rs 中的新main函数相当简单,看起来像真正的Rust代码:

  1. fn main() {
  2. let path = std::env::args_os().skip(1).next()
  3. .expect("usage: git-toy PATH");
  4. let repo = git::Repository::open(&path)
  5. .expect("opening repository");
  6. let commit_oid = repo.reference_name_to_id("HEAD")
  7. .expect("looking up 'HEAD' reference");
  8. let commit = repo.find_commit(&commit_oid)
  9. .expect("looking up commit");
  10. let author = commit.author();
  11. println!("{} <{}>\n",
  12. author.name().unwrap_or("(none)"),
  13. author.email().unwrap_or("none"));
  14. println!("{}", commit.message().unwrap_or("(none)"));
  15. }

在本节中,我们通过安排任何违反后者合同的违规行为为Rust类型的错误,在不安全的API上构建了一个安全的API.结果是Rust可以确保你正确使用的接口.在大多数情况下,我们使Rust强制执行的规则只是C和C++程序员最终强加于自己的规则.让Rust感觉比C和C++更严格的原因并不是规则如此陌生,而是执法是机械的和全面的.

结论(Conclusion)

Rust不是一种简单的语言.它的目标是跨越两个截然不同的世界.它是一种现代编程语言,设计安全,具有闭包和迭代器等便利性;但它旨在让你控制运行它的机器的原始功能,同时将运行时开销降至最低.

语言的轮廓由这些目标决定.Rust设法通过安全代码弥合大部分差距.它的借用检查器和零成本抽象使你尽可能接近裸机(bare metal),而不会有未定义行为风险.当这还不够时,或者当你想利用现有的C代码时,不安全的代码就可以了.但同样,该语言不仅为你提供这些不安全的功能,并祝你好运.目标始终是使用不安全的功能来构建安全的API.这就是我们对libgit2做的.这也是Rust团队用Box,Vec,其他集合,通道等做的事情:标准库充满了安全的抽象,在幕后用一些不安全代码实现.

具有Rust野心的语言可能并非注定是最简单的工具.但Rust是安全,快速,并发和高效的.使用它来构建大型,快速,安全,健壮的系统,充分利用其运行的硬件的全部功能.用它来改进软件.