不要有人认为我软弱无能,温良恭顺; 我恰好是另外一种女人: 我对仇人很强暴,对朋友却很温和, 要像我这样的为人才算光荣. --欧里庇得斯, <美狄亚>
原文
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::thread
和std::sync
模块提供并发原语.
本章介绍了使用不安全功能的基本要点:
-
Rust的
unsafe
块在普通的,安全的Rust代码和使用不安全功能的代码之间建立了界限. -
你可以将函数标记为
unsafe
,提醒调用者他们必须遵守的额外合同,以避免未定义行为. -
原始指针及其方法允许无限制地访问内存,并允许你构建数据结构,非不安全代码情况下,Rust的类型系统将禁止的数据结构.
-
理解未定义行为的定义将有助于你理解为什么它的后果远比仅仅获得不正确的结果更严重.
-
Rust的外部函数接口允许你使用其他语言编写的库.
-
类似于
unsafe
函数的不安全traits强加了一个合同,每个实现(而不是每个调用者)必须遵循.
在本书的开头,我们展示了一个以令人惊讶的方式崩溃的C程序,因为它没有遵循C标准规定的规则之一.你可以在Rust中执行相同的操作:
$ cat crash.rs
fn main() {
let mut a: usize = 0;
let ptr = &mut a as *mut usize;
unsafe {
*ptr.offset(3) = 0x7ffff72f484c;
}
}
$ cargo build
Compiling unsafe-samples v0.1.0
Finished debug [unoptimized + debuginfo] target(s) in 0.44 secs
$ ../../target/debug/crash
crash: Error: .netrc file is readable by others.
crash: Remove password or make file unreadable by others.
Segmentation fault (core dumped)
$
该程序借用对局部变量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
块看起来就像一个前面是unsafe
关键字的普通的Rust块,,区别在于你可以在块中使用不安全的功能:
unsafe {
String::from_utf8_unchecked(ascii)
}
如果没有块前面的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
的定义,这是一种字符串类型,可确保其内容始终是有效的ASCII.此类型使用不安全的功能来提供零成本转换为String
:
mod my_ascii {
use std::ascii::AsciiExt; // for u8::is_ascii
/// An ASCII-encoded string.
#[derive(Debug, Eq, PartialEq)]
pub struct Ascii(
// This must hold only well-formed ASCII text:
// bytes from `0` to `0x7f`.
Vec<u8>
);
impl Ascii {
/// Create an `Ascii` from the ASCII text in `bytes`. Return a
/// `NotAsciiError` error if `bytes` contains any non-ASCII
/// characters.
pub fn from_bytes(bytes: Vec<u8>) -> Result<Ascii, NotAsciiError> {
if bytes.iter().any(|&byte| !byte.is_ascii()) {
return Err(NotAsciiError(bytes))
}
Ok(Ascii(bytes))
}
}
// When conversion fails, we give back the vector we couldn't convert.
// This should implement `std::error::Error`; omitted for brevity.
#[derive(Debug, Eq, PartialEq)]
pub struct NotAsciiError(pub Vec<u8>);
// Safe, efficient conversion, implemented using unsafe code.
impl From<Ascii> for String {
fn from(ascii: Ascii) -> String {
// If this module has no bugs, this is safe, because
// well-formed ASCII text is also well-formed UTF-8.
unsafe { String::from_utf8_unchecked(ascii.0) }
}
}
...
}
该模块的关键是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
的缓冲区.
有了这些定义,你可以写:
use my_ascii::Ascii;
let bytes: Vec<u8> = b"ASCII and ye shall receive".to_vec();
// This call entails no allocation or text copies, just a scan.
let ascii: Ascii = Ascii::from_bytes(bytes)
.unwrap(); // We know these chosen bytes are ok.
// This call is zero-cost: no allocation, copies, or scans.
let string = String::from(ascii);
assert_eq!(string, "ASCII and ye shall receive");
使用Ascii
不需要unsafe
块.我们使用不安全的操作实现了一个安全的接口,并且只根据模块自己的代码而不是用户的行为安排来满足他们的合同.
Ascii
只不过是Vec<u8>
的包装器,隐藏在一个模块中,该模块强制执行有关其内容的额外规则.这种类型称为 newtype ,Rust中的常见模式.Rust自己的String
类型以完全相同的方式定义,只是它的内容被限制为UTF-8,而不是ASCII.实际上,这是标准库中String
的定义:
pub struct String {
vec: Vec<u8>,
}
在机器级别,在不显示Rust类型的情况下,newtype及其元素在内存中具有相同的表示,因此构造newtype根本不需要任何机器指令.在Ascii::from_bytes
中,表达式Ascii(bytes)
只是认为Vec<u8>
的表示现在保持Ascii值.类似地,String::from_utf8_unchecked
在内联时可能不需要机器指令:Vec<u8>
现在被认为是一个String
.
unsafe
函数定义看起来像一个普通的函数定义,前面是unsafe
关键字.unsafe
函数的主体自动被视为unsafe
块.
你可以仅在unsafe
块中调用unsafe
函数.这意味着标记函数unsafe
会警告其调用者该函数具有必须满足的约定以避免未定义行为.
例如,这里是我们之前介绍的Ascii
类型的新构造函数,它从字节向量构建Ascii
,而不检查其内容是否是有效的ASCII:
// This must be placed inside the `my_ascii` module.
impl Ascii {
/// Construct an `Ascii` value from `bytes`, without checking
/// whether `bytes` actually contains well-formed ASCII.
///
/// This constructor is infallible, and returns an `Ascii` directly,
/// rather than a `Result<Ascii, NotAsciiError>` as the `from_bytes`
/// constructor does.
///
/// # Safety
///
/// The caller must ensure that `bytes` contains only ASCII
/// characters: bytes no greater than 0x7f. Otherwise, the effect is
/// undefined.
pub unsafe fn from_bytes_unchecked(bytes: Vec<u8>) -> Ascii {
Ascii(bytes)
}
}
据推测,调用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
,如下所示:
// Imagine that this vector is the result of some complicated process
// that we expected to produce ASCII. Something went wrong!
let bytes = vec![0xf7, 0xbf, 0xbf, 0xbf];
let ascii = unsafe {
// This unsafe function's contract is violated
// when `bytes` holds non-ASCII bytes.
Ascii::from_bytes_unchecked(bytes)
};
let bogus: String = ascii.into();
// `bogus` now holds ill-formed UTF-8. Parsing its first character
// produces a `char` that is not a valid Unicode code point.
assert_eq!(bogus.chars().next().unwrap() as u32, 0x1fffff);
这说明了有关bugs和不安全代码的两个关键事实:
-
在
unsafe
块之前发生的bugs可能会破坏合同(Bugs that occur before theunsafe
block can break contracts) .unsafe
块是否导致未定义行为不仅取决于块本身的代码,还取决于提供其操作的值的代码.你的unsafe
代码依赖于满足合同的一切都是安全至关重要的.仅当模块的其余部分正确维护Ascii
的不变量时,才能很好地定义基于String::from_utf8_unchecked
从Ascii
到String
的转换. -
违约的后果可能会出现在离开
unsafe
块之后(the consequences of breaking a contract may appear after you leave theunsafe
block) .未遵守不安全功能合同所引起的未定义行为通常不会发生在unsafe
块内.如前所示构造伪造的String
可能不会导致问题,直到程序执行的后期.
从本质上讲,Rust的类型检查器,借用检查器和其他静态检查正在检查你的程序并尝试构建一个证明,证明它不能表现出未定义行为.当Rust成功编译你的程序时,这意味着它成功地证明了你的代码声音.一个unsafe
块是这个证明中的一个空白:"这段代码,"你对Rust说,"很好,相信我."你的主张是否真实可能取决于程序的任何部分,它们影响unsafe
块中发生的事情,并且出现错误的后果可能会出现在任何受unsafe
块影响的地方.编写unsafe
关键字相当于提醒你,你没有从语言的安全检查中获得全部好处.
鉴于选择,你自然应该更喜欢创建安全的接口,而无需合同.这些更容易使用,因为用户可以依靠Rust的安全检查来确保他们的代码没有未定义行为.即使你的实现使用不安全的功能,也最好使用Rust的类型,生命周期和模块系统来满足它们的合同,同时只使用你自己可以保证的内容,而不是将责任传递给你的调用者.
不幸的是,在野外遇到不安全函数并不罕见,其文档无需解释它们的合同.根据你的经验和代码行为的知识,你需要自己推断规则.如果你曾经不安地想知道你使用C或C++ API做的是否正常,那么你就知道它是什么样的.
你可能会发现自己想知道是使用unsafe
块还是只是标记整个函数不安全.我们建议的方法是先做出关于函数的决定:
-
如果以可编译的方式滥用函数但仍导致未定义行为,则必须将其标记为不安全.正确使用该函数的规则是其合同;合同的存在是使函数不安全的原因.
-
否则,该函数是安全的:没有对其进行良好类型的调用会导致未定义行为.它不应该被标记为
unsafe
.
函数是否在其主体中使用不安全的功能是无关紧要的;重要的是合同的存在.之前,我们展示了一个不安全函数,它不使用不安全的功能,以及一个使用不安全功能的安全函数.
不要因为在体使用不安全的功能而将安全函数标记为unsafe
.这使得该函数更难以使用,并且会使(正确地)期望找到合同解释的读者感到困惑.相反,使用unsafe
块,即使它是函数的整个主体.
在介绍中,我们说术语 未定义行为(undefined behavior) 意味着"Rust坚定地认为你的代码永远不会表现出来的行为(behavior that Rust firmly assumes your code could never exhibit)."这是一个奇怪的短语转变,特别是因为我们从其他语言的经验中知道这些行为 确实(do) 是偶然发生的,有一定频率.为什么这个概念有助于规定不安全代码的义务?
编译器是从一种编程语言到另一种编程语言的翻译器.Rust编译器接受Rust程序并将其转换为等效的机器语言程序.但是,说这种完全不同语言的两个程序是等价的是什么意思呢?
幸运的是,对于程序员而言,这个问题比语言学家更容易.我们通常说两个程序是等价的,如果它们在执行时总是具有相同的可见行为:它们进行相同的系统调用,以相同的方式与外部库交互,等等.这有点像程序的图灵测试:如果你不能分辨你是在与原始的还是翻译的进行交互,那么它们就是等价的.
现在考虑以下代码:
let i = 10;
very_trustworthy(&i);
println!("{}", i * 100);
即使对于very_trustworthy
的定义一无所知,我们也可以看到它只接收到对i
的共享引用,因此调用不能改变i
的值.由于传递给println!
的值将始终为1000
,Rust可以将此代码转换为机器语言,就像我们这样写的一样:
very_trustworthy(&10);
println!("{}", 1000);
此转换版本具有与原始版本相同的可见行为,并且可能更快一些.但只有在我们同意它与原版具有相同含义的情况下考虑此版本的性能才是有意义的.如果very_trustworthy
的定义如下?
fn very_trustworthy(shared: &i32) {
unsafe {
// Turn the shared reference into a mutable pointer.
// This is undefined behavior.
let mutable = shared as *const i32 as *mut i32;
*mutable = 20;
}
}
此代码违反了共享引用的规则:它将i
的值更改为20
,即使它应该被冻结,因为i
是共享借用.结果,我们对调用者的转换现在具有非常明显的效果:如果Rust转换代码,程序将打印1000
;如果它单独留下代码并使用i
的新值,则打印2000
.在very_trustworthy
中打破共享引用的规则意味着共享引用在其调用者中不会按预期运行.
这种问题出现在Rust可能尝试的几乎所有类型的转换中.甚至将函数内联到其调用点中也假设,当被调用者结束时,控制流返回到调用点.但是我们打开了这一章,其中包含了一个违反该假设的不良行为代码的例子.
Rust(或任何其他语言)基本上不可能评估程序的转换是否保留其含义,除非它可以信任该语言的基本功能以按设计行事.它们是否有所不同,不仅取决于手头的代码,还取决于其他(可能很遥远)的程序部分.为了对你的代码做任何事情,Rust必须假设你的程序的其余部分都表现良好.
这里是Rust的,对于表现良好的程序的规则:
-
程序不得读取未初始化的内存.
-
程序不得创建无效的原始值:
- 引用或boxes为
bull
bool
值不是0
或1
- 具有无效判别值的
enum
值 - 无效的
char
值,无代码的Unicode代码点 str
值不是格式良好的UTF-8
- 引用或boxes为
-
必须遵守第5章中解释的引用规则.没有任何引用可以活得比它引用的对象更久;共享访问是只读访问;并且可变访问是独占访问.
-
程序不得解引用
null
,错误对齐或悬空指针. -
程序不得使用指针访问与指针关联的分配之外的内存.我们将在第540页的"安全地解引用原始指针(Dereferencing Raw Pointers Safely)"中详细解释此规则.
-
程序必须没有数据竞争.当两个线程在没有同步的情况下访问相同的内存位置时,会发生数据竞争,并且至少有一个访问是写入.
-
程序不得通过外部函数接口,在另一种语言的调用中展开,如第146页的"展开(Unwinding)"中所述.
-
程序必须符合标准库函数的合同.
这些规则都是Rust在优化程序并将其转换为机器语言的过程中所假设的.简单来说,未定义行为违反了这些规则.这就是为什么我们说Rust假定你的程序不会出现未定义行为:如果我们希望得出结论编译的程序是源代码的忠实翻译,那么这个假设是必要的.
不使用不安全功能的Rust代码保证在编译后遵循所有前面的规则.一旦其编译.只有当你使用不安全的功能时,这些规则才会成为你的责任.在C和C++中,程序编译时没有错误或警告意味着更少;正如我们在本书的介绍中所提到的那样,即使是那些将代码保持在高标准的备受尊重的项目所编写的最好的C和C++程序在实践中也表现出不未定义行为.
不安全trait(unsafe trait) 是具有合同的trait,Rust无法检查或强制实现者必须满足以避免未定义行为.要实现不安全trait,必须将实现标记为不安全.由你来理解trait的合同,并确保你的类型满足它.
用不安全trait限制其类型变量的函数通常是使用不安全的功能的函数,并且仅通过依赖于不安全trait的契约来满足其合同.trait的不正确实现可能导致此类函数表现出未定义行为.
不安全traits的典型例子是std::marker::Send
和std::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,缺少方法或关联类型:
pub unsafe trait Zeroable {}
适当类型的实现同样简单明了:
unsafe impl Zeroable for u8 {}
unsafe impl Zeroable fori32 {}
unsafe impl Zeroable for usize {}
// and so on for all the integer types
有了这些定义,我们可以编写一个函数,快速分配包含Zeroable
类型的给定长度的向量:
#![feature(nonzero)]
// permits `Zeroable`
extern crate core;
use core::nonzero::Zeroable;
fn zeroed_vector<T>(len: usize) -> Vec<T>
where T: Zeroable
{
let mut vec = Vec::with_capacity(len);
unsafe {
std::ptr::write_bytes(vec.as_mut_ptr(), 0, len);
vec.set_len(len);
}
vec
}
此函数首先创建一个具有所需容量的空Vec
,然后调用write_bytes
以用零填充未占用的缓冲区.(write_byte
函数将len
视为T
元素的个数,而不是字节的个数,因此该调用会填充整个缓冲区.)向量的set_len
方法更改其长度而不对缓冲区执行任何操作;这是不安全的,因为你必须确保新封闭的缓冲区空间实际上包含类型为T
的正确初始化值.但这正是T: Zeroable
限制所建立的:零字节块表示有效的T
值.我们使用set_len
是安全的.
在这里,我们使用它:
let v: Vec<usize> = zeroed_vector(100_000);
assert!(v.iter().all(|&u| u == 0));
显然,Zeroable
必须是不安全trait,因为不遵守其合同的实现可能导致未定义行为:
struct HoldsRef<'a>(&'a mut i32);
unsafe impl<'a> Zeroable for HoldsRef<'a> { }
let mut v: Vec<HoldsRef> = zeroed_vector(1);
*v[0].0 = 1; // crashes: dereferences null pointer
Rust在没有抱怨的情况下编译它:它不知道Zeroable
意味着什么,所以它无法判断它何时被用于不适当的类型.与任何其他不安全功能一样,由你来理解并遵守不安全trait的合同.
请注意,不安全的代码不能依赖于正确实现的普通,安全特性.例如,假设有一个std::hash::Hasher
trait的实现,它只返回一个随机哈希值,与被哈希的值无关.该trait要求对两次相同的位进行哈希必须产生相同的哈希值,但此实现不符合该要求;这完全是错误的.但是因为Hasher
不是一个不安全trait,所以当使用这个hasher时,不安全代码不得表现出未定义行为.std::collections::HashMap
类型是经过精心编写的,以尊重它使用的不安全功能的合同,无论hasher的行为如何.当然,该表将无法正常运行:查找将失败,并且条目将随机出现和消失.但该表不会显示未定义行为.
Rust中的 原始指针(raw pointer) 是一个不受约束的指针.你可以使用原始指针来形成Rust的检查指针类型不能的各种结构,例如双向链表或对象的任意图形.但由于原始指针非常灵活,Rust无法判断你是否安全使用它们,因此你只能在unsafe
块中解引用它们.
原始指针本质上等同于C或C++指针,因此它们对于与用这些语言编写的代码进行交互也很有用.有两种原始指针:
-
*mut T
是指向T
的原始指针,允许修改其引用的对象. -
*const T
是指向T的原始指针,只允许读取其引用的对象.
(没有普通的*T
类型;你必须始终指定const
或mut
.)
你可以通过从引用转换来创建原始指针,并使用*
运算符解引用它:
let mut x = 10;
let ptr_x = &mut x as *mut i32;
let y = Box::new(20);
let ptr_y = &*y as *const i32;
unsafe {
*ptr_x += *ptr_y;
}
assert_eq!(x, 30);
与boxes和引用不同,原始指针可以为空(null),如C中的NULL
或C++中的nullptr
:
fn option_to_raw<T>(opt: Option<&T>) -> *const T {
match opt {
None => std::ptr::null(),
Some(r) => r as *const T
}
}
assert!(!option_to_raw(Some(&("pea", "pod"))).is_null());
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::Debug
和std::fmt::Pointer
,它们将原始指针显示为十六进制地址,而不解引用它们.
与C和C++中的+
运算符不同,Rust的+
不处理原始指针,但你可以通过其offset
和wrapping_offset
方法执行指针运算.找到两个指针之间的距离没有标准操作,就像-
运算符在C和C++中那样,但你可以自己编写一个:
fn distance<T>(left: *const T, right: *const T) -> isize {
(left as isize - right as isize) / std::mem::size_of::<T>() as isize
}
let trucks = vec!["garbage truck", "dump truck", "moonstruck"];
let first = &trucks[0];
let last = &trucks[2];
assert_eq!(distance(last, first), 2);
assert_eq!(distance(first, last), -2);
即使distance
的参数是原始指针,我们也可以将引用传递给它:Rust隐含地强制引用原始指针(当然不是反过来).
as
运算符允许从引用到原始指针或两个原始指针类型之间的几乎所有合理的转换.但是,你可能需要将复杂的转换分解为一系列简单的步骤.例如:
&vec![42_u8] as *const String// error: invalid conversion
&vec![42_u8] as *const Vec<u8> as *const String; // permitted
请注意,as
不会将原始指针转换为引用.这样的转换将是不安全的,as
应该保持安全操作.相反,你必须解引用原始指针(在unsafe
块中),然后借用结果值.
执行此操作时要非常小心:以这种方式生成的引用具有不受约束的生命周期:它可以存活多长时间没有限制,因为原始指针使得Rust无法做出这样的决定.在本章后面的第572页的"libgit2的安全接口(A Safe Interface to libgit2)"中,我们将展示如何正确约束生命周期的几个示例.
许多类型都有as_ptr
和as_mut_ptr
方法,它们返回指向其内容的原始指针.例如,数组切片和字符串返回指向其第一个元素的指针,一些迭代器返回指向它们将生成的下一个元素的指针.像Box
,Rc
和Arc
这样的拥有指针类型有into_raw
和from_raw
函数,可以转换为原始指针和从原始指针转换来.其中一些方法的合同带来了令人惊讶的要求,因此在使用它们之前请查阅它们的文档.
你也可以通过从整数转换来构造原始指针,尽管你可以信任的唯一整数通常是你首先从指针获得的整数.第541页的"示例:RefWithFlag(Example: RefWithFlag)"以这种方式使用原始指针.
与引用不同,原始指针既不是Send
也不是Sync
.因此,默认情况下,包含原始指针的任何类型都不会实现这些traits.在线程之间发送或共享原始指针没有什么本质上不安全的;毕竟,无论它们走到哪里,你仍然需要一个unsafe
块来解引用它们.但是考虑到原始指针通常扮演的角色,语言设计者认为这种行为是更有用的默认行为.我们已经在第536页的"不安全Traits(Unsafe Traits)"中讨论了如何自己实现Send
和Sync
.
以下是一些安全使用原始指针的常识性指南:
-
解引用空指针或悬空指针是未定义行为,因为指向未初始化的内存或超出作用域的值.
-
解引用未针对其引用的对象类型正确对齐的指针是未定义行为.
-
只有在遵守第5章中解释的引用安全规则的情况下,你才可以从解引用的原始指针借用值:没有引用可能比它的引用的对象活得更久;共享访问是只读访问;可变访问是独占访问.(此规则很容易被违反,因为原始指针通常用于创建具有非标准共享或所有权的数据结构.)
-
只有当它是格式良好的其类型的值时,才可以使用原始指针的引用的对象.例如,你必须确保解引用
*const char
会产生正确的,非代理(nonsurrogate)的Unicode代码点. -
你可以在原始指针上使用
offset
和wrapping_offset
方法,仅指向原始指针所引用的变量或堆分配的内存块中的字节,或指向此区域之外的第一个字节.
如果通过将指针转换为整数来进行指针运算,对整数进行算术运算,然后将其转换回指针,则结果必须是offset
方法的规则允许你生成的指针.
- 如果赋值给原始指针的引用对象,则不得违反引用对象所属的任何类型的不变量.例如,如果你有一个
*mut u8
指向一个String
的一个字节,你可能只存储u8
中的值,使得String
保持格式良好的UTF-8.
除了借用规则,这些规则与在C或C++中使用指针时必须遵循的规则基本相同.
不违反类型不变量的原因应该是清楚的.Rust的许多标准类型在其实现中使用不安全代码,但仍然提供安全的接口,假设Rust的安全检查,模块系统和可见性规则将得到尊重.使用原始指针来规避这些保护措施可能导致未定义行为.
原始指针的完整,准确的合约不容易说明,并且可能随着语言的发展而变化.但是这里列出的原则应该让你处于安全的境地.
下面是一个示例,说明如何通过原始指针实现经典1的位级别的hack,并将其包装为完全安全的Rust类型.这个模块定义了一个类型,RefWithFlag<'a, T>
,它同时包含一个&'a T
和一个bool
,就像元组`(&'a T, bool),但仍然设法只占用一个机器字而不是两个.这种技术经常用于垃圾收集器和虚拟机,其中某些类型--如表示对象的类型--如此之多,以至于每个值添加单个字会大大增加内存使用量:
mod ref_with_flag {
use std::marker::PhantomData;
use std::mem::align_of;
/// A `&T` and a `bool`, wrapped up in a single word.
/// The type `T` must require at least two-byte alignment.
///
/// If you're the kind of programmer who's never met a pointer whose
/// 2⁰-bit you didn't want to steal, well, now you can do it safely!
/// ("But it's not nearly as exciting this way...")
pub struct RefWithFlag<'a, T: 'a> {
ptr_and_bit: usize,
behaves_like: PhantomData<&'a T> // occupies no space
}
impl<'a, T: 'a> RefWithFlag<'a, T> {
pub fn new(ptr: &'a T, flag: bool) -> RefWithFlag<T> {
assert!(align_of::<T>() % 2 == 0);
RefWithFlag {ptr_and_bit: ptr as *const T asusize | flag as usize,
behaves_like: PhantomData
}
}
pub fn get_ref(&self) -> &'a T {
unsafe {
let ptr = (self.ptr_and_bit & !1) as *const T;
&*ptr
}
}
pub fn get_flag(&self) -> bool {
self.ptr_and_bit & 1 != 0
}
}
}
这段代码利用了许多类型必须放在内存中偶数地址的事实:因为偶数地址的最低有效位总是为零,我们可以在那里存储其他东西,然后通过屏蔽底位可靠地重建原始地址位.并非所有类型都符合条件;例如,类型u8
和(bool, [i8; 2])
可以放在任何地址.但是我们可以在构造时检查类型的对齐情况,拒绝不起作用的类型.
你可以像这样使用RefWithFlag
:
use ref_with_flag::RefWithFlag;
let vec = vec![10, 20, 30];
let flagged = RefWithFlag::new(&vec, true);
assert_eq!(flagged.get_ref()[1], 20);
assert_eq!(flagged.get_flag(), true);
构造函数RefWithFlag::new
接受引用和bool
值,断言引用的类型是合适的,然后将引用转换为原始指针,然后转换为usize
.usize
类型被定义为足够大以在我们正在编译的任何处理器上保存指针,因此将原始指针转换为usize
和转回是良好定义的.一旦我们有了一个usize
,我们知道它必须是偶数,所以我们可以使用|
按位或运算符将它与bool
组合,我们已将其转换为整数0
或1
.
get_flag
方法提取RefWithFlag
的bool
组件.这很简单:只需掩盖底位并检查它是否非零.
get_ref
方法从RefWithFlag
中提取引用.首先,它掩盖了usize
的底位并将其转换为原始指针.as
运算符不会将原始指针转换为引用,但是我们可以解引用原始指针(当然是在一个unsafe
块中)并借用它.借用一个原始指针的引用对象为你提供一个无限生命周期的引用:Rust会给出引用,无论生命周期会使它周围的代码检查,如果有的话.但是,通常情况下,某些特定的生命周期更准确,因此会出现更多错误.在这种情况下,由于get_ref
的返回类型是&'a T
,Rust推断引用的生命周期必须是RefWithFlag
的参数,这正是我们想要的:这是我们开始的引用的生命周期.
在内存中,RefWithFlag
看起来就像一个usize
:由于PhantomData
是一个零大小的类型,所以behaves_like
字段在结构中不占用空间.但是,PhantomData
是Rust知道如何在使用RefWithFlag
的代码中处理生命周期的必要条件.想象一下没有behaves_like
字段时类型会是什么样子:
// This won't compile.
pub struct RefWithFlag<'a, T: 'a> {
ptr_and_bit: usize
}
在第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会抱怨参数'a
和T
未使用,并建议使用PhantomData
.
RefWithFlag
使用与我们之前介绍的Ascii
类型相同的策略,以避免其unsafe
块中的未定义行为.类型本身是pub
,但它的字段不是,,意味着只有pointer_and_bool
模块中的代码可以创建或查看RefWithFlag
值.你不必检查太多代码就可以确信ptr_and_bit
字段构造良好.
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>
值.
任何Sized
类型的值在内存中占用恒定的字节数,并且必须放置在由机器体系结构确定的某个 对齐(alignment) 值的倍数的地址处.例如,(i32, i32)
元组占用8个字节,并且大多数处理器更喜欢将其放置在4的倍数的地址处.
调用std::mem::size_of::<T>()
返回类型为T
的值的大小(以字节为单位),std::mem::align_of::<T>()
返回其所需的对齐.例如:
assert_eq!(std::mem::size_of::<i64>(), 8);
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_val
和std::mem::align_of_val
函数返回值的大小和对齐.这些函数可以对Sized
和无大小类型的引用进行操作.
// Fat pointers to slices carry their referent's length.
let slice: &[i32] = &[1, 3, 9, 27, 81];
assert_eq!(std::mem::size_of_val(slice), 20);
let text: &str = "alligator";
assert_eq!(std::mem::size_of_val(text), 9);
use std::fmt::Display;
let unremarkable: &Display = &193_u8;
let remarkable: &Display = &0.0072973525664;
// These return the size/alignment of the value the
// trait object points to, not those of the trait object
// itself. This information comes from the vtable the
// trait object refers to.
assert_eq!(std::mem::size_of_val(unremarkable), 1);
assert_eq!(std::mem::align_of_val(remarkable), 8);
Rust将数组,切片或向量的元素作为单个连续的内存块进行布局,如图21-1所示.元素是规则间隔的,因此如果每个元素占用size
字节,则第i
个元素以第i * size
字节开始.
图21-1. 内存种的数组.
这样做的一个好结果是,如果你有两个指向数组元素的原始指针,比较指针会得到与比较元素索引相同的结果:如果i < j
,则指向第i
个元素的原始指针小于指向第j
个元素的原始指针.这使得原始指针可用作数组遍历的边界.实际上,标准库在切片上的简单迭代器定义如下:
struct Iter<'a, T: 'a> {
ptr: *const T,
end: *const T,
...
}
ptr
字段指向迭代应该产生的下一个元素,并且end
字段用作限制:当ptr == end
时,迭代完成.
数组布局的另一个好结果:如果element_ptr
是一个*const T
或*mut T
原始指针,它指向某个数组的第i
个元素,那么element_ptr.offset(o)
是一个指向第(i + o)
个元素的原始指针.它的定义等同于:
fn offset(self: *const T, count: isize) -> *const T
where T: Sized
{
let bytes_per_element = std::mem::size_of::<T>() as isize;
let byte_offset = count * bytes_per_element;
(self as isize).checked_add(byte_offset).unwrap() as *const T
}
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
本身的相对排序.当然,除非它们属于数组,否则你仍然无法解引用这些指针.
如果要实现管理自己内存的类型,则需要跟踪内存的哪些部分保存活着的值,哪些部分未初始化,就像Rust处理局部变量一样.考虑以下代码:
let pot = "pasta".to_string();
let plate;
plate = pot;
运行此代码后,情况如图21-2所示.
图21-2. 将字符串从一个局部变量移动到另一个局部变量.
赋值后,pot
是未初始化的,plate
是字符串的所有者.
在机器级别,没有指定移动对源的作用,但在实践中,它通常什么都不做.该赋值可能使pot
仍然保持字符串的指针,容量和长度.当然.将此视为活着的值将是灾难性的,Rust确保你不这样做.
同样的考虑适用于管理自己内存的数据结构.假设你运行此代码:
let mut noodles = vec!["udon".to_string()];
let soba = "soba".to_string();
let last;
在内存中,状态如图21-3所示.
图21-3. 一个具有未初始化的,备用容量的向量.
向量具有容纳一个元素的备用容量,但其内容是垃圾,可能是先前存储的内存.假设你然后运行此代码:
noodles.push(soba);
将字符串压入向量会将未初始化的内存转换为新元素,如图21-4所示:
图21-4. 将soba的值推入向量上之后.
向量已初始化其空白空间以拥有该字符串,并增加其长度以将其标记为新的,活着的元素.向量现在是字符串的所有者;你可以引用它的第二个元素,并且删除向量将释放两个字符串.soba
现在是未初始化的.
最后,考虑一下当我们从向量中弹出一个值时会发生什么:
last = noodles.pop().unwrap();
在内存中,现在看起来如图21-5所示.
图21-5.将一个元素从向量弹出到last之后.
变量last
取得了字符串的所有权.向量已减少其长度,以指示用于保存字符串的空间现在未初始化.
就像之前的pot
和pasta
一样,所有三个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
原始指针并且value
为T
值,其中T
是有大小的类型.
这是Vec::push
背后的操作.推入值会调用write
以将值移动到下一个可用空间,然后递增长度以将该空间标记为有效元素.
两者都是自由函数,而不是原始指针类型的方法.
请注意,你不能使用Rust的任何安全指针类型来执行这些操作.它们都需要在任何时候初始化它们的引用对象,因此将未初始化的内存转换为值(反之亦然),是他们无法实现的.原始指针符合要求.
标准库还提供了将值的数组从一个内存块移动到另一个内存块的函数:
-
std::ptr::copy(src, dst, count)
将从src
开始的内存中的count
值的数组移动到dst
的内存中,就好像你已经编写了一个read
和write
调用循环来移动它们,一次一个.在调用之前,目标内存必须是未初始化的,之后源内存将保持未初始化状态.src
和dest
参数必须是*const T
和*mut T
原始指针,count
必须是usize
. -
std::ptr::copy_nonoverlapping(src, dst, count)
就像对应的copy
调用一样,只是它的契约还要求内存的源块和目标块不能重叠.这可能比调用copy
稍快.
还有另外两个read
和write
函数系列,也在std::ptr
模块中:
-
read_unaligned
和write_unaligned
函数类似于read
和write
,只是指针不需要像引用对象类型通常所需的那样对齐.这些函数可能比普通的read
和write
函数慢. -
read_volatile
和write_volatile
函数相当于C或C++中的易失性(volatile)读写.
这是一个将刚刚描述的原始指针函数投入使用的示例.
假设你正在编写文本编辑器,你正在寻找一种表示文本的类型.你可以选择String
,并使用insert
和remove
方法在用户键入时插入和删除字符.但是如果他们在大文件的开头编辑文本,那么这些方法可能很昂贵:插入一个新字符需要将字符串的其余部分移到内存中的右边,删除将它全部移回到左边.你希望这种常见的操作更便宜.
Emacs文本编辑器使用一种称为 间隙缓冲区(gap buffer) 的简单数据结构,可以在恒定时间内插入和删除字符.尽管String
在文本末尾保留了所有备用容量,这使得push
和pop
便宜,但是在编辑正在发生的时候,间隙缓冲区将其备用容量保持在文本中间.这个备用容量称为 间隙(gap) .在间隙处插入或删除元素很便宜:你只需根据需要缩小或扩大间隙.你可以通过将文本从间隙的一侧移动到另一侧来将间隙移动到你喜欢的任何位置.当间隙为空时,你将迁移到更大的缓冲区.
虽然间隙缓冲区中的插入和删除速度很快,但更改它们发生的位置需要将间隙移动到新位置.移动元素需要的时间与移动距离成比例.幸运的是,典型的编辑活动需要在离开缓冲区之前对缓冲区的一个邻域中进行一系列更改,然后在其他地方修改文本.
在本节中,我们将在Rust中实现间隙缓冲区.为了避免被UTF-8分心,我们将直接使缓冲区存储char
值,但如果我们以其他形式存储文本,则操作原理将是相同的.
首先,我们将展示一个运行种的间隙缓冲区.此代码创建一个GapBuffer
,在其中插入一些文本,然后将插入点移动到最后一个单词之前:
use gap::GapBuffer;
let mut buf = GapBuffer::new();
buf.insert_iter("Lord of the Rings".chars());
buf.set_position(12);
运行此代码后,缓冲区如图21-6所示.
图21-6. 包含一些文本的间隙缓冲区.
插入是用新文本填补间隙的问题.这段代码添加了一个单词并破坏了电影(原单词:lord of the rings(指环王))):
buf.insert_iter("Onion ".chars());
图21-7. 包含更多文本的间隙缓冲区.
这是我们的GapBuffer
类型:
mod gap {
use std;
use std::ops::Range;
pub struct GapBuffer<T> {
// Storage for elements. This has the capacity we need, but its length
// always remains zero. GapBuffer puts its elements and the gap in this
// `Vec`'s "unused" capacity.
storage: Vec<T>,
// Range of uninitialized elements in the middle of `storage`.
// Elements before and after this range are always initialized.
gap: Range<usize>
}
...
}
GapBuffer以奇怪的方式2使用其存储字段.它实际上从未在向量中存储任何元素--或者不完全存储.它只是调用Vec::with_capacity(n)
来获取足够大的内存块以容纳n
个值,通过向量的as_ptr
和as_mut_ptr
方法获得指向该内存的原始指针,然后直接使用缓冲区用于其自身目的.向量的长度始终为零.当Vec
被删除时,Vec
不会试图释放它的元素,因为它不知道它有任何,但它确实释放了内存块.这就是GapBuffer
想要的;它有自己的Drop
实现,它知道活着的元素的位置并正确删除它们.
GapBuffer
最简单的方法就是你所期望的:
impl<T> GapBuffer<T> {
pub fn new() -> GapBuffer<T> {
GapBuffer { storage: Vec::new(), gap: 0..0 }
}
/// Return the number of elements this GapBuffer could hold without
/// reallocation.
pub fn capacity(&self) -> usize {
self.storage.capacity()
}
/// Return the number of elements this GapBuffer currently holds.
pub fn len(&self) -> usize {
self.capacity() - self.gap.len()
}
/// Return the current insertion position.
pub fn position(&self) -> usize {
self.gap.start
}
...
}
它清除了以下许多函数,以便有一个工具方法,它返回给定索引处的缓冲区元素的原始指针.这是Rust,我们最终需要一个用于mut
指针的方法和一个用于const
指针的方法.与前面的方法不同,这些方法不公有.继续这个impl
块:
/// Return a pointer to the `index`'th element of the underlying storage,
/// regardless of the gap.
///
/// Safety: `index` must be a valid index into `self.storage`.
unsafe fn space(&self, index: usize) -> *const T {
self.storage.as_ptr().offset(index as isize)
}
/// Return a mutable pointer to the `index`'th element of the underlying
/// storage, regardless of the gap.
///
/// Safety: `index` must be a valid index into `self.storage`.
unsafe fn space_mut(&mut self, index: usize) -> *mut T {
self.storage.as_mut_ptr().offset(index as isize)
}
要在给定索引处查找元素,你必须考虑索引是在间隙之前还是之后,并进行适当调整:
/// Return the offset in the buffer of the `index`'th element, taking
/// the gap into account. This does not check whether index is in range,
/// but it never returns an index in the gap.
fn index_to_raw(&self, index: usize) -> usize {
if index < self.gap.start {
index
} else {
index + self.gap.len()
}
}
/// Return a reference to the `index`'th element,
/// or `None` if `index` is out of bounds.
pub fn get(&self, index: usize) -> Option<&T> {
let raw = self.index_to_raw(index);
if raw < self.capacity() {
unsafe {
// We just checked `raw` against self.capacity(),
// and index_to_raw skips the gap, so this is safe.
Some(&*self.space(raw))
}
} else {
None
}
}
当我们开始在缓冲区的不同部分进行插入和删除时,我们需要将间隙移动到新位置.向右移动间隙需要将元素向左移动,反之亦然,就像当流体在另一个方向上流动时,水平面中的气泡向一个方向移动:
/// Set the current insertion position to `pos`.
/// If `pos` is out of bounds, panic.
pub fn set_position(&mut self, pos: usize) {
if pos > self.len() {
panic!("index {} out of range for GapBuffer", pos);
}
unsafe {
let gap = self.gap.clone();
if pos > gap.start {
// `pos` falls after the gap. Move the gap right
// by shifting elements after the gap to before it.
let distance = pos - gap.start;
std::ptr::copy(self.space(gap.end),
self.space_mut(gap.start),
distance);
} else if pos < gap.start {
// `pos` falls before the gap. Move the gap left
// by shifting elements before the gap to after it.
let distance = gap.start - pos;
std::ptr::copy(self.space(pos),
self.space_mut(gap.end - distance),
distance);
}
self.gap = pos .. pos + gap.len();
}
}
该函数使用std::ptr::copy
方法移动元素;copy
要求目标未初始化,并使源未初始化.源和目标范围可能重叠,但copy
正确地处理该情况.由于在调用之前间隙是未初始化的内存,并且函数调整间隙的位置以覆盖副本腾出的空间,因此满足copy
函数的合同.
元素插入和移除相对简单.插入为新元素占据间隙的一个空间.而移除将一个值移出,并扩大间隙以覆盖它曾经占用的空间:
/// Insert `elt` at the current insertion position,
/// and leave the insertion position after it.
pub fn insert(&mut self, elt: T) {
if self.gap.len() == 0 {
self.enlarge_gap();
}
unsafe {
let index = self.gap.start;
std::ptr::write(self.space_mut(index), elt);
}
self.gap.start += 1;
}
/// Insert the elements produced by `iter` at the current insertion
/// position, and leave the insertion position after them.
pub fn insert_iter<I>(&mut self, iterable: I)
where I: IntoIterator<Item=T>
{
for item in iterable {
self.insert(item)
}
}
/// Remove the element just after the insertion position
/// and return it, or return `None` if the insertion position
/// is at the end of the GapBuffer.
pub fn remove(&mut self) -> Option<T> {
if self.gap.end == self.capacity() {
return None;
}
let element = unsafe {
std::ptr::read(self.space(self.gap.end))
};
self.gap.end += 1;
Some(element)
}
类似于Vec
使用std::ptr::write
用于推入(push)和std::ptr::read
用于弹出(pop)的方式,GapBuffer
使用write
来插入,read
来移除remove
.正如Vec
必须调整其长度以保持初始化的元素和备用容量之间的边界一样,GapBuffer
会调整其间隙.
填充间隙后,insert
方法必须增加缓冲区以获得更多可用空间.enlarge_gap
方法(impl
块中的最后一个)处理这个:
/// Double the capacity of `self.storage`.
fn enlarge_gap(&mut self) {
let mut new_capacity = self.capacity() * 2;
if new_capacity == 0 {
// The existing vector is empty.
// Choose a reasonable starting capacity.
new_capacity = 4;
}
// We have no idea what resizing a Vec does with its "unused"
// capacity. So just create a new vector and move over the elements.
let mut new = Vec::with_capacity(new_capacity);
let after_gap = self.capacity() - self.gap.end;
let new_gap = self.gap.start .. new.capacity() - after_gap;
unsafe {
// Move the elements that fall before the gap.
std::ptr::copy_nonoverlapping(self.space(0),
new.as_mut_ptr(),
self.gap.start);
// Move the elements that fall after the gap.
let new_gap_end = new.as_mut_ptr().offset(new_gap.end as isize);
std::ptr::copy_nonoverlapping(self.space(self.gap.end),
new_gap_end,
after_gap);
}
// This frees the old Vec, but drops no elements,
// because the Vec's length is zero.
self.storage = new;
self.gap = new_gap;
}
而set_position
必须使用copy
来在间隙中来回移动元素,而enlarge_gap
可以使用copy_nonoverlapping
,因为它将元素移动到一个新的缓冲区.
将新向量移动到self.storage
会删除旧向量.由于它的长度为零,因此旧向量认为它没有要删除的元素,只是释放它的缓冲区.很巧妙地,copy_nonoverlapping
保留了源为未初始化,因此旧的向量是正确的:所有元素现在都由新向量拥有.
最后,我们需要确保删除GapBuffer
会删除其所有元素:
impl<T> Drop for GapBuffer<T> {
fn drop(&mut self) {
unsafe {
for i in 0 .. self.gap.start {
std::ptr::drop_in_place(self.space_mut(i));
}
for i in self.gap.end .. self.capacity() {
std::ptr::drop_in_place(self.space_mut(i));
}
}
}
}
元素位于间隙之前和之后,因此我们遍历每个区域并使用std::ptr::drop_in_place
函数删除每个区域.drop_in_place
函数是一个行为类似于drop(std :: ptr :: read(ptr))
的工具,但不打扰将值移动到其调用者(因此适用于无大小的类型).就像在enlarge_gap
中一样,当向量self.storage
被删除时,它的缓冲区确实是未初始化的.
与我们在本章中展示的其他类型一样,GapBuffer
确保其自身的不变量足以确保遵循其使用的每个不安全功能的合同,因此其公有方法都不需要标记为不安全.GapBuffer
为无法在安全代码中有效编写的功能实现安全接口.
在Rust中,恐慌通常不会导致未定义行为;panic!
宏不是一个不安全功能.但是,当你决定使用不安全的代码时,恐慌安全就会成为你工作的一部分.
考虑上一节中的GapBuffer::remove
方法:
pub fn remove(&mut self) -> Option<T> {
if self.gap.end == self.capacity() {
return None;
}
let element = unsafe {
std::ptr::read(self.space(self.gap.end))
};
self.gap.end += 1;
Some(element)
}
调用read
会在间隙离开缓冲区之后立即移动元素,留下未初始化的空间.幸运的是,下一个语句扩大了间隙以覆盖该空间,所以当我们返回时,一切都应该是这样:间隙之外的所有元素都被初始化,并且间隙内的所有元素都是未初始化的.
但是考虑一下,如果在调用read
之后但在调整self.gap.end
之前,这段代码试图使用可能会出现恐慌的功能--比如索引切片.这会发生什么.在这两个动作之间的任何地方突然退出该方法将使GapBuffer
在间隙之外留下未初始化的元素.下一次remove
调用可能会尝试再次read
它;甚至只是删除GapBuffer
就会试图删除它.两者都是未定义的行为,因为它们访问未初始化的内存.
对于类型的方法来说,在他们完成工作的同时暂时放松类型的不变量,然后在返回之前将所有内容恢复到正确,这几乎是不可避免的.恐慌的中间方法可以缩短清理过程,使类型处于不一致状态.
如果类型仅使用安全代码,则此不一致可能会使类型行为异常,但不会引入未定义行为.但是使用不安全功能的代码通常依靠其不变量来满足这些功能的合同.破坏不变量导致合同破环,从而导致未定义行为.
使用不安全功能时,你必须特别注意识别这些敏感区域,并确保它们不会做任何可能引起恐慌的事情.
Rust的 外部函数接口(foreign function interface) 允许Rust代码调用用C或C++编写的函数.
在本节中,我们将编写一个与libgit2
链接的程序,libgit2
是一个用于处理Git版本控制系统的C库.首先,我们将展示直接从Rust使用C函数的情况.然后,我们将展示如何构建一个安全的libgit2
接口,从开源git2-rs
crate中获取灵感.
我们假设你熟悉C以及编译和链接C程序的机制.使用C++是类似的.我们还假设你对Git版本控制系统有点熟悉.
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_char
是i8
或u8
. -
没有与C的
bool
对应的确定的Rust类型.目前,Rustbool
始终为零或一个字节,与所有主要C和C++实现使用的表示相同.但是,Rust语言团队未承诺在将来保留此表示形式,因为这样做可能会关闭优化机会. -
Rust的32位
char
类型不是wchar_t
的类似物,其宽度和编码因实现而异.C的char32_t
类型更接近,但其编码仍然不能保证是Unicode. -
Rust的原始
usize
和isize
类型与C的size_t
和ptrdiff_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结构类型一样.例如,libgit2
的 git2/errors.h 头文件定义了以下C结构,以提供有关先前报告的错误的详细信息:
typedef struct {
char *message;
int klass;
} git_error;
你可以使用相同的表示定义Rust类型,如下所示:
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int
}
#[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风格枚举的表示形式:
#[repr(C)]
enum git_error_code {
GIT_OK = 0,
GIT_ERROR = -1,
GIT_ENOTFOUND = -3,
GIT_EEXISTS = -4,
...
}
通常,Rust在选择如何表示枚举时会播放各种游戏.例如,我们提到了Rust用来在单个字中存储Option<&T>
的技巧(如果T
是有大小的).如果没有#[repr(C)]
,Rust将使用单个字节来表示git_error_code
枚举;使用#[repr(C)]
,Rust使用一个Cint
大小的值,就像C一样.
你还可以要求Rust为枚举提供与某种整数类型相同的表示形式.使用#[repr(i16)]
开始前面的定义将为你提供一个16位类型,其表示形式与以下C++枚举相同:
#include <stdint.h>
enum git_error_code: int16_t {
GIT_OK = 0,
GIT_ERROR = -1,
GIT_ENOTFOUND = -3,
GIT_EEXISTS = -4,
...
};
在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
模块中,CString
和CStr
类型表示拥有的和借用的以null终止的字节数组.与String
和str
相比,CString
和CStr
上的方法非常有限,仅限于构造和转换为其他类型.我们将在下一节中展示这些类型.
extern
块声明在一些其他库中定义的函数或变量,最终的Rust可执行文件将与之链接.例如,每个Rust程序都链接到标准C库,因此我们可以告诉Rust关于C库的strlen
函数,如下所示:
use std::os::raw::c_char;
extern {
fn strlen(s: *const c_char) -> usize;
}
这为Rust提供了函数的名称和类型,同时保留了稍后要链接的定义.
Rust假定在extern
块中声明的函数使用C约定来传递参数和接受返回值.它们被定义为unsafe
函数.这对strlen
来说是正确选择:它确实是一个C函数;并且它在C中的规范要求你将一个指向正确终止的字符串的有效指针传递给它,这是Rust无法强制执行的合同.(几乎所有接受原始指针的函数都必须是unsafe
:安全的Rust可以从任意整数构造原始指针,解引用这样的指针将是未定义行为.)
有了这个extern
块,我们可以像任何其他Rust函数一样调用strlen
,尽管它的类型可以作为游客使用它:
use std::ffi::CString;
let rust_str = "I'll be back";
let null_terminated = CString::new(rust_str).unwrap();
unsafe {
assert_eq!(strlen(null_terminated.as_ptr()), 12);
}
CString::new
函数构建一个以null终止的C字符串.它首先检查其嵌入空字符的参数,因为它们不能在C字符串中表示,并且如果找到任何字符则返回错误(因此需要unwrap
结果).否则,它会在结尾添加一个空字节,并返回一个拥有结果字符的CString
.
CString::new
的成本取决于你传递的类型.它接受任何实现Into<Vec<u8>>
的东西.传递&str
需要分配和副本,因为转换为Vec<u8>
会为要拥有的向量构建一个堆分配的字符串副本.但是通过值传递String
只是消耗字符串并接管其缓冲区,因此除非附加空字符强制缓冲区调整大小,否则转换根本不需要复制文本或分配.
你还可以在extern
块中声明全局变量.POSIX系统有一个名为environ
的全局变量,它保存进程环境变量的值.在C中,它声明了:
extern char **environ;
在Rust中,你可以说:
use std::ffi::CStr;
use std::os::raw::c_char;
extern {
static environ: *mut *mut c_char;
}
要打印环境的第一个元素,你可以编写:
unsafe {
if !environ.is_null() && !(*environ).is_null() {
let var = CStr::from_ptr(*environ);
println!("first environment variable: {}",
var.to_string_lossy())
}
}
在确保environ
具有第一个元素之后,代码调用CStr::from_ptr
来构建一个借用它的CStr
.to_string_lossy
方法返回一个Cow<str>
:如果C字符串包含格式良好的UTF-8,则Cow
将其内容借用为&str
,不包括终止null字节.否则,to_string_lossy
会在堆中创建文本的副本,用官方的Unicode替换字符'�''
替换格式错误的UTF-8序列,并从中构建一个拥有的Cow
.无论哪种方式,结果都实现了Display
,因此你可以使用{}
格式化参数进行打印.
要使用特定库提供的函数,可以在extern
块的顶部放置一个#[link]
属性,命名Rust应该将可执行文件链接到的库.例如,这是一个调用libgit2
的初始化和关闭方法的程序,但不执行任何其他操作:
use std::os::raw::c_int;
#[link(name = "git2")]
extern {
pub fn git_libgit2_init() -> c_int;
pub fn git_libgit2_shutdown() -> c_int;
}
fn main() {
unsafe {
git_libgit2_init();
git_libgit2_shutdown();
}
}
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.org 和 https://www.python.org 下载.
有关构建libgit2
的完整说明可在其网站上找到,但它们非常简单,我们将在此处显示基本要素.在Linux上,假设你已经将库的源解压缩到目录 /home/jimb/libgit2-0.25.1 中:
$ cd /home/jimb/libgit2-0.25.1
$ mkdir build
$ cd build
$ cmake ..
$ 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命令提示符中:
> cd C:\Users\JimB\libgit2-0.25.1
> mkdir build
> cd build
> cmake -A x64 ..
> 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程序:
$ cd /home/jimb
$ cargo new --bin git-toy
将上面的代码放在 src/main.rs 中.当然,如果你尝试构建它,Rust不知道在哪里可以找到你构建的libgit2
:
$ cd git-toy
$ cargo run
Compiling git-toy v0.1.0 (file:///home/jimb/git-toy)
error: linking with `cc` failed: exit code: 1
|
= note: "cc" ... "-l" "git2" ...
= note: /usr/bin/ld: cannot find -lgit2
collect2: error: ld returned 1 exit status
error: aborting due to previous error
error: Could not compile `git-toy`.
To learn more, run the command again with --verbose.
$
你可以通过编写 构建脚本(build script) 来告诉Rust在哪里搜索库,即Cargo编译并在构建时运行的Rust代码.构建脚本可以做各种事情:动态生成代码,编译C代码,包含在crate中,等等.在这种情况下,你只需要在可执行文件的链接命令中添加库搜索路径.当Cargo运行构建脚本时,它会解析构建脚本的输出以获取此类信息,因此构建脚本只需要将正确的魔法打印到其标准输出.
要创建构建脚本,请在 Cargo.toml 文件所在的目录中添加名为 build.rs 的文件,其中包含以下内容:
fn main() {
println!(r"cargo:rustc-link-search=native=/home/jimb/libgit2-0.25.1/build");
}
这是Linux的正确途径;在Windows上,你可以将文本native =
之后的路径更改为C:\Users\JimB\libgit2-0.25.1\build\Debug
(为了简化这个例子,我们做了一些偷工减料的工作;在实际应用程序中,你应该避免在构建脚本中使用绝对路径.我们在本节末尾引用了说明如何正确执行该操作的文档.)
接下来,通过将行build = "build.rs"
添加到 Cargo.toml 文件的[package]
部分,告诉Cargo这是你的构建脚本.整个文件现在应该是:
[package]
name = "git-toy"
version = "0.1.0"
authors = ["You <[email protected]>"]
build = "build.rs"
[dependencies]
现在你几乎可以运行该程序了.在macOS上它可以立即工作;在Linux系统上,你可能会看到如下内容:
$ cargo run
Compiling git-toy v0.1.0 (file:///home/jimb/git-toy)
Finished dev [unoptimized + debuginfo] target(s) in 0.64 secs
Running `target/debug/git-toy`
target/debug/git-toy: error while loading shared libraries:
libgit2.so.25: cannot open shared object file: No such file or directory
$
这意味着,虽然Cargo成功地将可执行文件链接到库,但它不知道在运行时在何处查找共享库.Windows通过弹出对话框报告此故障.在Linux上,你必须设置LD_LIBRARY_PATH
环境变量:
$ export LD_LIBRARY_PATH=/home/jimb/libgit2-0.25.1/build:$LD_LIBRARY_PATH
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/git-toy`
$
在macOS上,你可能需要设置DYLD_LIBRARY_PATH
.
在Windows上,你必须设置PATH
环境变量:
> set PATH=C:\Users\JimB\libgit2-0.25.1\build\Debug;%PATH%
> cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/git-toy`
>
当然,在已部署的应用程序中,你需要避免仅为查找库的代码而设置环境变量.另一种方法是将C库静态链接到你的crate中.这会将库的目标文件复制到crate的 .rlib 文件中,以及crate的Rust代码的目标文件和元数据.然后整个集合参与最后的链接.
Cargo约定,提供对C库的访问的crate应该命名为LIB-sys
,其中LIB
是C库的名称.-sys
crate应该只包含静态链接库和包含extern
块和类型定义的Rust模块.然后,更高级别的接口属于依赖于-sys
包的crate.这允许多个上游crates依赖于相同的-sys
crate,假设有一个单个版本的-sys
crate满足每个人的需求.
有关Cargo支持构建脚本和链接系统库的完整详细信息,请参阅在线Cargo文档.它显示了如何避免构建脚本中的绝对路径,控制编译标志,使用pkg-config
等工具,等等.git2-rs
包也提供了很好的例子来模拟;它的构建脚本处理一些复杂的情况.
弄清楚如何正确使用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
的文件,其内容如下:
#![allow(non_camel_case_types)]
use std::os::raw::{c_int, c_char, c_uchar};
#[link(name = "git2")]
extern {
pub fn git_libgit2_init() -> c_int;
pub fn git_libgit2_shutdown() -> c_int;
pub fn giterr_last() -> *const git_error;
pub fn git_repository_open(out: *mut *mut git_repository,
path: *const c_char) -> c_int;
pub fn git_repository_free(repo: *mut git_repository);
pub fn git_reference_name_to_id(out: *mut git_oid,
repo: *mut git_repository,
reference: *const c_char) -> c_int;
pub fn git_commit_lookup(out: *mut *mut git_commit,
repo: *mut git_repository,
id: *const git_oid) -> c_int;
pub fn git_commit_author(commit: *const git_commit) -> *const git_signature;
pub fn git_commit_message(commit: *const git_commit) -> *const c_char;
pub fn git_commit_free(commit: *mut git_commit);
}
pub enum git_repository {}
pub enum git_commit {}
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int}
#[repr(C)]
pub struct git_oid {
pub id: [c_uchar; 20]
}
pub type git_time_t = i64;
#[repr(C)]
pub struct git_time {
pub time: git_time_t,
pub offset: c_int
}
#[repr(C)]
pub struct git_signature {
pub name: *const c_char,
pub email: *const c_char,
pub when: git_time
}
这里的每一项都是根据libgit2
自己的头文件的声明建模的.例如, libgit2-0.25.1/include/git2/repository.h 包含以下声明:
extern int git_repository_open(git_repository **out, const char *path);
此函数尝试在path
打开Git存储库.如果一切顺利,它会创建一个git_repository
对象,并在out
指向的位置存储指向它的指针.等效的Rust声明如下:
pub fn git_repository_open(out:*mut *mut git_repository,
path:*const c_char) -> c_int;
libgit2
公有头文件将git_repository
类型定义为不完整结构类型的typedef:
typedef struct git_repository git_repository;
由于此类型的详细信息对库是私有的,因此公有头文件永远不会定义struct git_repository
,从而确保库的用户永远不会自己构建此类型的实例.Rust中不完整的结构类型的一个可能类似于:
pub enum git_repository {}
这是一个没有变体的枚举类型.Rust中没有办法制作这种类型的值.这是一个奇怪的问题,但它完美地反映了只有libgit2
应该构造的C类型,并且只能通过原始指针进行操作.
手工编写大型extern
块可能是件苦差事.如果要为复杂的C库创建Rust接口,则可能需要尝试使用bindgen
crate,它具有可以从构建脚本中使用的函数来解析C头文件并自动生成相应的Rust声明.我们没有空间在这里显示bindgen
,但是crate.io上的bindgen
页面包含其文档的链接.
接下来我们将完全重写 main.rs .首先,我们需要声明raw
模块:
mod raw;
根据libgit2
的约定,易错函数返回一个整数代码,该代码在成功时为正或零,在失败时为负.如果发生错误,giterr_last
函数将返回指向git_error
结构的指针,它提供有关错误的更多详细信息.libgit2
拥有这个结构,所以我们不需要自己释放它,但它可以被我们下一个库调用覆盖.一个正确的Rust接口会使用Result
,但在原始版本中,我们希望使用libgit2
函数就如它们本来的样子,因此我们必须使用自己的函数来处理错误:
use std::ffi::CStr;
use std::os::raw::c_int;
fn check(activity: &'static str, status: c_int) -> c_int {
if status < 0 {
unsafe {
let error = &*raw::giterr_last();
println!("error while {}: {} ({})",
activity,
CStr::from_ptr(error.message).to_string_lossy(),
error.klass);
std::process::exit(1);
}
}
status
}
我们将使用此函数来检查libgit2
调用的结果,如下所示:
check("initializing library", raw::git_libgit2_init());
这使用了和前面使用的相同的CStr
方法:from_ptr
从C字符串构造CStr
,而to_string_lossy
将其转换为Rust可以打印的东西.
接下来,我们需要一个函数来打印commit:
unsafe fn show_commit(commit: *const raw::git_commit) {
let author = raw::git_commit_author(commit);
let name = CStr::from_ptr((*author).name).to_string_lossy();
let email = CStr::from_ptr((*author).email).to_string_lossy();
println!("{} <{}>\n", name, email);
let message = raw::git_commit_message(commit);
println!("{}", CStr::from_ptr(message).to_string_lossy());
}
给定指向git_commit
的指针,show_commit
调用git_commit_author
和git_commit_message
来检索它需要的信息.这两个函数遵循libgit2
文档解释如下的约定:
如果函数返回一个对象作为返回值,则该函数是一个getter,并且该对象的生命周期与父对象相关联.
在Rust术语中,author
和message
是从commit
中借来的:show_commit
不需要自己释放它们,但是在释放commit
后它不能保留它们.由于这个API使用原始指针,Rust不会为我们检查它们的生命周期:如果我们不小心创建了悬空指针,我们可能会在程序崩溃之前找不到它.
前面的代码假定这些字段包含UTF-8文本,这并不总是正确的.Git也允许其他编码.正确解释这些字符串可能需要使用encoding
crate.为了简洁起见,我们将在这里掩饰这些问题.
我们程序的main
函数如下:
use std::ffi::CString;
use std::mem;
use std::ptr;
use std::os::raw::c_char;
fn main() {
let path = std::env::args().skip(1).next()
.expect("usage: git-toy PATH");
let path = CString::new(path)
.expect("path contains null characters");
unsafe {
check("initializing library", raw::git_libgit2_init());
let mut repo = ptr::null_mut();
check("opening repository",
raw::git_repository_open(&mut repo, path.as_ptr()));
let c_name = b"HEAD\0".as_ptr() as *const c_char;
let mut oid = mem::uninitialized();
check("looking up HEAD",
raw::git_reference_name_to_id(&mut oid, repo, c_name));
let mut commit = ptr::null_mut();
check("looking up commit",
raw::git_commit_lookup(&mut commit, repo, &oid));
show_commit(commit);
raw::git_commit_free(commit);
raw::git_repository_free(repo);
check("shutting down library", raw::git_libgit2_shutdown());
}
}
这开始于处理路径参数和初始化库的代码,我们之前已经看到了所有这些.第一个新颖的代码是这样的:
let mut repo = ptr::null_mut();
check("opening repository",
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的对象哈希码:
let mut oid = mem::uninitialized();
check("looking up HEAD",
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
用于repo
和commit
变量,但由于这些只是单个字而uninitialized
使用起来如此冒险,我们只需将它们初始化为null:
let mut commit = ptr::null_mut();
check("looking up commit",
raw::git_commit_lookup(&mut commit, repo, &oid));
这将获取commit的对象标识符并查找实际commit,并在成功时在commit
中存储git_commit
指针.
main
函数的其余部分应该是不言自明的.它调用前面定义的show_commit
函数,释放commit和存储库对象,并关闭库.
现在我们可以在任何准备好的Git存储库上试用该程序:
$ cargo run /home/jimb/rbattle
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/git-toy /home/jimb/rbattle`
Jim Blandy <[email protected]>
Animate goop a bit.
$
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
模块,其中前一个程序的原始接口是私有子模块.
整个源代码树将如下所示:
git-toy/
├── Cargo.toml
├── build.rs
└── src/
├── main.rs
└── git/
├── mod.rs
└── raw.rs
遵循我们在第166页的"单独文件中的模块(Modules in Separate Files)"中解释的规则,git
模块的源代码显示在 git/mod.rs 中,其git::raw
子模块的源代码位于 git/raw.rs 中.
再一次,我们将完全重写 main.rs .它应该从git
模块的声明开始:
mod git;
然后,我们需要创建 git 子目录,并将 raw.rs 移动到其中:
$ cd /home/jimb/git-toy
$ mkdir src/git
$ mv src/raw.rs src/git/raw.rs
git
模块需要声明其raw
子模块.文件 src/git/mod.rs 必须说:
mod raw;
由于它不是pub
,因此主程序看不到该子模块.
稍后我们需要使用libc
crate中的一些函数,因此我们必须在 Cargo.toml 中添加一个依赖项.完整文件现在显示为:
[package]
name = "git-toy"
version = "0.1.0"
authors = ["Jim Blandy <[email protected]>"]
build = "build.rs"
[dependencies]
libc = "0.2.23"
相应的extern crate
项必须出现在 src/main.rs 中:
extern crate libc;
现在我们重新构建了模块,让我们考虑错误处理.即使libgit2
的初始化函数也可以返回错误码,因此我们需要在开始之前对其进行整理.惯用的Rust接口需要自己的Error
类型,它捕获libgit2
失败码以及giterr_last
中的错误消息和类.正确的错误类型必须实现通常的Error
,Debug
和Display
trait.然后,它需要使用此Error
类型的自己的Result
类型.以下是 src/git/mod.rs 中的必要定义:
use std::error;
use std::fmt;
use std::result;
#[derive(Debug)]
pub struct Error {
code: i32,
message: String,
class: i32
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
// Displaying an `Error` simply displays the message from libgit2.
self.message.fmt(f)
}
}
impl error::Error for Error {
fn description(&self) -> &str { &self.message }
}
pub type Result<T> = result::Result<T, Error>;
要检查原始库调用的结果,模块需要一个将libgit2
返回代码转换为Result
的函数:
use std::os::raw::c_int;
use std::ffi::CStr;
fn check(code: c_int) -> Result<c_int> {
if code >= 0 {
return Ok(code);
}
unsafe {
let error = raw::giterr_last();
// libgit2 ensures that (*error).message is always non-null and null
// terminated, so this call is safe.
let message = CStr::from_ptr((*error).message)
.to_string_lossy()
.into_owned();
Err(Error {
code: code as i32,
message,
class: (*error).klass as i32
})
}
}
这与原始版本的check
函数之间的主要区别在于,它构造了一个Error
值,而不是打印错误消息并立即退出.
现在我们已准备好解决libgit2
初始化问题.安全接口将提供表示开放Git存储库的Repository
类型,其中包含用于解析引用,查找commit等的方法.继续 git/mod.rs ,这里是Repository
的定义:
/// A Git repository.
pub struct Repository {
// This must always be a pointer to a live `git_repository` structure.
// No other `Repository` may point to it.
raw: *mut raw::git_repository
}
Repository
的raw
字段不公有.由于只有此模块中的代码可以访问raw::git_repository
指针,因此正确使用此模块应确保指针始终正确使用.
如果创建Repository
的唯一方法是成功打开一个新的Git存储库,那么将确保每个Repository
指向一个不同的git_repository
对象:
use std::path::Path;
impl Repository {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Repository> {
ensure_initialized();
let path = path_to_cstring(path.as_ref())?;
let mut repo = null_mut();
unsafe {
check(raw::git_repository_open(&mut repo, path.as_ptr()))?;
}
Ok(Repository { raw: repo })
}
}
由于使用安全接口执行任何操作的唯一方法是从Repository
值开始,并且Repository::open
以调用ensure_initialized
开始,因此我们可以确信在任何libgit2
函数之前将调用ensure_initialized
.其定义如下:
use std;
use libc;
fn ensure_initialized() {
static ONCE: std::sync::Once = std::sync::ONCE_INIT;
ONCE.call_once(|| {
unsafe {
check(raw::git_libgit2_init())
.expect("initializing libgit2 failed");
assert_eq!(libc::atexit(shutdown), 0);
}
});
}
use std::io::Write;
extern fn shutdown() {
unsafe {
if let Err(e) = check(raw::git_libgit2_shutdown()) {
let _ = writeln!(std::io::stderr(),
"shutting down libgit2 failed: {}",
e);
std::process::abort();
}
}
}
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)"中,我们提到了跨语言边界的恐慌是未定义行为.从atexit
到shutdown
的调用是这样的边界,因此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
值:
impl Drop for Repository {
fn drop(&mut self) {
unsafe {
raw::git_repository_free(self.raw);
}
}
}
通过仅在指向raw::git_repository
的唯一指针即将消失时调用git_repository_free
,Repository
类型还确保指针在释放后永远不会被使用.
Repository::open
方法使用一个名为path_to_cstring
的私有函数,它有两个定义--一个用于类Unix系统.一个用于Windows:
use std::ffi::CString;
#[cfg(unix)]
fn path_to_cstring(path: &Path) -> Result<CString> {
// The `as_bytes` method exists only on Unix-like systems.
use std::os::unix::ffi::OsStrExt;
Ok(CString::new(path.as_os_str().as_bytes())?)
}
#[cfg(windows)]
fn path_to_cstring(path: &Path) -> Result<CString> {
// Try to convert to UTF-8. If this fails, libgit2 can't handle the path
// anyway.
match path.to_str() {
Some(s) => Ok(CString::new(s)?),
None => {
let message = format!("Couldn't convert path '{}' to UTF-8",
path.display());
Err(message.into())
}
}
}
libgit2
接口使这段代码有点棘手.在所有平台上,libgit2
接受路径为以null终止的C字符串.在Windows上,libgit2
假设这些C字符串保存格式良好的UTF-8,并在内部将它们转换为Windows实际需要的16位路径.这通常有效,但并不理想.Windows允许文件名不是格式良好的Unicode,因此无法用UTF-8表示.如果你有这样的文件,则无法将其名称传递给libgit2
.
在Rust中,文件系统路径的正确表示是std::path::Path
,经过精心设计,可以处理Windows或POSIX上可以出现的任何路径.这意味着Windows上存在无法传递给libgit2
的Path
值,因为它们不是格式良好的UTF-8.因此虽然path_to_cstring
的行为不太理想,但实际上我们可以在libgit2的接口上做到最好.
刚刚显示的两个path_to_cstring
定义依赖于我们的Error
类型的转换:?
运算符尝试此类转换,Windows版本显式调用.into()
.这些转换不起眼:
impl From<String> for Error {
fn from(message: String) -> Error {
Error { code: -1, message, class: 0 }
}
}
// NulError is what `CString::new` returns if a string
// has embedded zero bytes.
impl From<std::ffi::NulError> for Error {
fn from(e: std::ffi::NulError) -> Error {
Error { code: -1, message: e.to_string(), class: 0 }
}
}
接下来,让我们弄清楚如何解析对象标识符的Git引用.由于对象标识符只是一个20字节的哈希值,因此在安全的API中公开它是完全正确的:
/// The identifier of some sort of object stored in the Git object
/// database: a commit, tree, blob, tag, etc. This is a wide hash of the
/// object's contents.
pub struct Oid {
pub raw: raw::git_oid
}
我们将向Repository
添加一个方法来执行查找:
use std::mem::uninitialized;
use std::os::raw::c_char;
impl Repository {
pub fn reference_name_to_id(&self, name: &str) -> Result<Oid> {
let name = CString::new(name)?;
unsafe {
let mut oid = uninitialized();
check(raw::git_reference_name_to_id(&mut oid, self.raw,
name.as_ptr() as *const c_char))?;
Ok(Oid { raw: oid })
}
}
}
虽然oid
在查找失败时保持未初始化,但是这个函数通过遵循Rust的Result
习惯用法来保证它的调用者不会看到未初始化的值:调用者得到一个带有正确初始化的Oid
值的Ok
,或者它得到一个Err
.
接下来,该模块需要一种从存储库中检索commits的方法.我们将定义一个Commit
类型,如下所示:
use std::marker::PhantomData;
pub struct Commit<'repo> {
// This must always be a pointer to a usable `git_commit` structure.
raw: *mut raw::git_commit,
_marker: PhantomData<&'repo Repository>
}
正如我们前面提到的,git_commit
对象必须永远不会活得超过从中检索它的git_repository
对象.Rust的生命周期让代码准确地捕获了这个规则.
本章前面的RefWithFlag
示例使用PhantomData
字段告诉Rust将类型视为包含具有给定生命周期的引用,即使该类型显然不包含此类引用.Commit
类型需要做类似的事情.在这种情况下,_marker
字段的类型是PhantomData<&'repo Repository>
,表明Rust应该将Commit<'repo>
视为保存带有生命周期'repo
的对某个Repository
的引用.
查找commits的方法如下:
use std::ptr::null_mut;
impl Repository {
pub fn find_commit(&self, oid: &Oid) -> Result<Commit> {
let mut commit = null_mut();
unsafe {
check(raw::git_commit_lookup(&mut commit, self.raw, &oid.raw))?;
}
Ok(Commit { raw: commit, _marker: PhantomData })
}
}
这如何将Commit
的生命周期与Repository
的生命周期联系起来?find_commit
的签名根据第112页中"省略生命周期参数(Omitting Lifetime Parameters)"概述的规则省略了所涉及引用的生命周期.如果我们要写出生命周期,那么完整的签名将是:
fn find_commit<'repo, 'id>(&'repo self, oid: &'id Oid)
-> Result<Commit<'repo>>
这正是我们想要的:Rust将返回的Commit
视为从self
(即Repository
)借用的内容.
当一个Commit
被删除时,它必须释放它的raw::git_commit
:
impl<'repo> Drop for Commit<'repo> {
fn drop(&mut self) {
unsafe {
raw::git_commit_free(self.raw);
}
}
}
在Commit
种,你可以借用Signature
(名称和电子邮件地址)和提交消息的文本:
impl<'repo> Commit<'repo> {
pub fn author(&self) -> Signature {
unsafe {
Signature {
raw: raw::git_commit_author(self.raw),
_marker: PhantomData
}
}
}
pub fn message(&self) -> Option<&str> {
unsafe {
let message = raw::git_commit_message(self.raw);
char_ptr_to_str(self, message)
}
}
}
这是Signature
类型:
pub struct Signature<'text> {
raw: *const raw::git_signature,
_marker: PhantomData<&'text str>
}
git_signature
对象总是从其他地方借用它的文本;特别是,git_commit_author
返回的签名从git_commit
中借用了它们的文本.所以我们的安全Signature
类型包括一个PhantomData<&'text str>
来告诉Rust表现得好像它包含一个带有'text'
生命周期的&str
.和以前一样,Commit::author
正确地将它返回的Signature
的'text
生命周期连接到Commit的
,而不需要编写任何东西.Commit::message
方法对包含提交消息的Option<&str>
执行相同的操作.
Signature
包含检索作者姓名和电子邮件地址的方法:
impl<'text> Signature<'text> {
/// Return the author's name as a `&str`,
/// or `None` if it is not well-formed UTF-8.
pub fn name(&self) -> Option<&str> {
unsafe {
char_ptr_to_str(self, (*self.raw).name)
}
}
/// Return the author's email as a `&str`,
/// or `None` if it is not well-formed UTF-8.
pub fn email(&self) -> Option<&str> {
unsafe {
char_ptr_to_str(self, (*self.raw).email)
}
}
}
上述方法依赖于私有工具函数char_ptr_to_str
:
/// Try to borrow a `&str` from `ptr`, given that `ptr` may be null or
/// refer to ill-formed UTF-8. Give the result a lifetime as if it were
/// borrowed from `_owner`.
///
/// Safety: if `ptr` is non-null, it must point to a null-terminated C
/// string that is safe to access.
unsafe fn char_ptr_to_str<T>(_owner: &T, ptr: *const c_char) -> Option<&str> {
if ptr.is_null() {
return None;
} else {
CStr::from_ptr(ptr).to_str().ok()
}
}
_owner
参数的值从未使用过,但它的生命周期就在那里.此函数的签名中的生命周期显式是这样的:
fn char_ptr_to_str<'o, T: 'o>(_owner: &'o T, ptr: *const c_char)
-> Option<&'o str>
CStr::from_ptr
函数返回一个&CStr
,它的生命周期是完全无界的,因为它是从一个解引用的原始指针借来的.无界的生命周期几乎总是不准确的,所以尽快约束它们是好的.包含_owner
参数会导致Rust将其生命周期归因于返回值的类型,因此调用者可以接收更准确的有界引用.
从libgit2
文档中不清楚git_signature
的email
和author
指针是否可以为null,尽管libgit2
的文档非常好.你的作者在源代码中挖掘了一段时间而无法以某种方式说服自己,最后决定为了以防万一,char_ptr_to_str
最好为空指针做好准备.在Rust中,这类问题会立即通过类型回答:如果是&str
,你可以指望字符串在那里;如果它是Option<&str>
,那么它是可选的.
最后,我们为所需的所有功能提供了安全的接口. src/main.rs 中的新main
函数相当简单,看起来像真正的Rust代码:
fn main() {
let path = std::env::args_os().skip(1).next()
.expect("usage: git-toy PATH");
let repo = git::Repository::open(&path)
.expect("opening repository");
let commit_oid = repo.reference_name_to_id("HEAD")
.expect("looking up 'HEAD' reference");
let commit = repo.find_commit(&commit_oid)
.expect("looking up commit");
let author = commit.author();
println!("{} <{}>\n",
author.name().unwrap_or("(none)"),
author.email().unwrap_or("none"));
println!("{}", commit.message().unwrap_or("(none)"));
}
在本节中,我们通过安排任何违反后者合同的违规行为为Rust类型的错误,在不安全的API上构建了一个安全的API.结果是Rust可以确保你正确使用的接口.在大多数情况下,我们使Rust强制执行的规则只是C和C++程序员最终强加于自己的规则.让Rust感觉比C和C++更严格的原因并不是规则如此陌生,而是执法是机械的和全面的.
Rust不是一种简单的语言.它的目标是跨越两个截然不同的世界.它是一种现代编程语言,设计安全,具有闭包和迭代器等便利性;但它旨在让你控制运行它的机器的原始功能,同时将运行时开销降至最低.
语言的轮廓由这些目标决定.Rust设法通过安全代码弥合大部分差距.它的借用检查器和零成本抽象使你尽可能接近裸机(bare metal),而不会有未定义行为风险.当这还不够时,或者当你想利用现有的C代码时,不安全的代码就可以了.但同样,该语言不仅为你提供这些不安全的功能,并祝你好运.目标始终是使用不安全的功能来构建安全的API.这就是我们对libgit2
做的.这也是Rust团队用Box
,Vec
,其他集合,通道等做的事情:标准库充满了安全的抽象,在幕后用一些不安全代码实现.
具有Rust野心的语言可能并非注定是最简单的工具.但Rust是安全,快速,并发和高效的.使用它来构建大型,快速,安全,健壮的系统,充分利用其运行的硬件的全部功能.用它来改进软件.