Rust中的unsafe

Go unsafe包

file:///Users/fliter/rust-in-databend/34-%E7%AC%AC%E5%85%AB%E8%AF%BE-unsafe-rust/%E7%AC%AC%E5%85%AB%E8%AF%BE%20unsafe%20rust.pdf

任何 trait,只要声明成 unsafe,它就是⼀个 unsafe trait


unsafe trait、unsafe函数、解引⽤裸指针、FFI 都是什么?有何作用


  • unsafe trait: Rust标准库中定义的一个trait,用于标记安全性方面的隐式安全承诺。主要用于内部实现中,便于代码组合。

  • unsafe函数: 使用unsafe修饰的函数,允许在函数体内执行一些不安全操作,比如解引用裸指针等。需要开发者保证函数内部安全性。

  • 解引用裸指针: 直接访问内存中的值,如 *ptr,而ptr指向空内存会导致panic。这是一种不安全操作。

  • Foreign Function Interface(FFI):Rust编写的外部函数接口(foreign function interface),允许Rust程序直接调用C/C++等外部语言编写的代码。

它们的作用和关系:

  • unsafe函数可以包含不安全操作,如解引用裸指针。

  • 通过FFI,可以在Rust中定义和调用外部语言(C/C++)函数,这往往需要使用unsafe block。

  • 一些底层操作如访问操作系统功能也需要通过unsafe实现。

  • unsafe trait用于描述代码是否包含隐式不安全承诺,主要用于内部安全保证。

总体来说,unsafe相关概念授予开发者一定的“特权”,以实现一些Rust本身无法直接支持但又有必要的不安全操作,从而扩展Rust的应用范围。


解引用裸指针可能导致panic,那么在Rust中如何确保解引用的裸指针是有效的?


在Rust中有以下几种方法可以确保解引用的裸指针是有效的,不会导致panic:

  1. 使用Option来包装指针
1
2
3
4
let p: Option<&i32> = Some(&x);
if let Some(v) = p {
// 解引用指针
}
  1. 匹配NULL指针
1
2
3
4
5
6
let p: *const i32 = NULL;
if p.is_null() {
// error
} else {
// 解引用指针
}
  1. 范围检查
1
2
3
4
let slice = &arr[0..len]; 
if index < len {
let v = unsafe { *slice.get_unchecked(index) };
}
  1. 断言指针有效性
1
2
3
4
unsafe {
assert!(!ptr.is_null());
// 解引用指针
}
  1. 使用result检查返回值
1
2
3
4
5
6
7
8
fn get_ptr() -> Result<*const i32, Error> {
// ..
Ok(ptr)
}

if let Ok(ptr) = get_ptr() {
// 解引用指针
}

总之,使用Option、结果类型、NULL检查和范围检查等手段,可以在解引用前验证指针的安全性,避免导致panic。



有没有一些示例代码可以展示如何使用FFI在Rust中调用C/C++函数?


这里有一个简单的例子展示如何在Rust中通过FFI调用C函数:

1
2
3
4
5
6
7
8
9
10
11
12
// C函数声明
#[link(name = "c")]
extern "C" {
fn greet() -> *const i8;
}

fn main() {
// 调用C函数
unsafe {
println!("Greeting from C: {}", greet());
}
}

C函数定义:

1
2
3
4
// greet.c
const char* greet() {
return "Hello from C!";
}

构建并运行:

1
2
3
4
$ gcc -c greet.c -o greet.o
$ rustc main.rs greet.o
$ ./main
Greeting from C: Hello from C!

关键点:

  • 使用extern “C”声明C接口
  • C函数返回值需要匹配
  • 调用 unsafe block包围,因为存在指针操作
  • 链接C对象文件

这就是一个简单的Rust调用C函数的例子。更复杂的情况可以传参、定义结构体映射等进行交互。

FFI让Rust与其他语言协作,扩展了它的应用范围。



可以在Rust中使用FFI调用其他编程语言的函数吗?


是的,Rust通过FFI(外部函数接口) mechanim可以调用其他主要编程语言定义的函数,不限于C/C++。

具体支持情况:

  • C/C++: 是最基本和成熟的支持,C函数签名可以直接在Rust中使用。

  • C#: 通过crate调用.NET代码,需要生成中间绑定。

  • Java: 通过JNI或crate调用Java代码,如rust-jni、jna等。

  • Python: 通过crate调用Python代码,如rust-cpython绑定库。

  • JavaScript: 通过wasm-bindgen调用WebAssembly模块定义的JS函数。

  • Lua: 通过luncheon绑定调用Lua脚本和库。

  • Go: 通过crate调用Go代码,如rust-bindgen。

  • Rust调用Rust: 通过crate生成桥接代码直接调用。

总体来说,只要语言可以生成接口定义或输出二进制码,rust都可以通过标准FFI机制或第三方crate进行调用。

例如定义C接口后,通过链接动态库实现多语言协作。这大大扩展了Rust并行和集成其他语言的能力。

注意FFI总会涉及一定的复杂性,选择合适的crate绑定库可以简化开发。




2024.07.02 记录

unsafe Rust - 不安全Rust - Rust的另一面

到目前为止,我们讨论的所有代码都享受着Rust在编译时强制执行的内存安全保证。然而,Rust内部隐藏着第二种语言,它不强制执行这些内存安全保证。它被称为不安全的Rust(unsafe Rust)。它的工作方式和常规的Rust一样,但赋予了我们额外的超能力。

不安全的Rust之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定代码是否遵守这些保证时,它宁愿拒绝一些有效的程序,尽管代码可能是没问题的。但如果Rust编译器没有足够的信息来确信,它就会拒绝该代码。在这些情况下,你可以使用不安全的代码来告诉编译器:”相信我,我知道我在做什么”。

但是请注意,使用不安全的Rust是有风险的。如果你错误地使用不安全的代码,可能会因为内存不安全而出现问题,比如对空指针进行解引用。

unsafe Rust存在的另一个原因是底层计算机硬件本质上是不安全的。如果Rust不允许你执行不安全的操作,你将无法完成某些任务。Rust需要允许你进行低级系统编程,比如直接与操作系统交互,甚至编写自己的操作系统。进行低级系统编程是该语言的目标之一。

让我们探索我们可以用不安全的Rust做什么,以及如何进行操作。要切换到不安全的Rust,使用unsafe关键字,然后开始一个新的代码块来包含不安全的代码。

在不安全的Rust中,你可以执行五种在安全Rust中无法进行的操作。我们称之为不安全的超能力。这些超能力包括:

  1. 解引用原始指针
  2. 调用不安全的函数或方法
  3. 访问或修改可变的静态变量
  4. 实现不安全的trait
  5. 访问联合体的字段

重要的是要理解,unsafe关键字并不会关闭借用检查器或禁用Rust的其他安全检查。如果你在不安全的代码中使用引用,它仍然会被检查。unsafe关键字只是让你能够访问这五个编译器不会检查内存安全的特性。在不安全的代码块中,你仍然会获得一定程度的安全性。

此外,unsafe并不意味着代码块内的代码一定是危险的,或者它肯定会有内存安全问题。作为开发者,你将确保在unsafe块内的代码将以有效的方式访问内存。人们会犯错误,错误会发生,但通过要求这五种不安全操作必须在用unsafe标注的代码块内进行,你将知道任何与内存安全相关的错误必须在unsafe块内。最好保持unsafe块尽可能小,当你以后调查内存错误时,你会感激的。

为了尽可能地隔离不安全代码,最好将不安全代码封装在一个安全的抽象中,并提供一个安全的API。标准库的部分内容是作为对经过审核的不安全代码的安全抽象实现的。

让我们依次看看这五种不安全的超能力。我们还将看看一些提供对不安全代码安全接口的抽象。

  1. 解引用原始指针

我们提到过编译器确保引用总是有效的。不安全的Rust引入了两种新的类型,称为原始指针。它们类似于引用。与引用一样,原始指针可以是不可变的或可变的,并分别写作const T和mut T。这里的星号不是解引用操作符,它是类型名称的一部分。在原始指针的上下文中,不可变意味着指针在解引用后不能直接被赋值。

与引用和智能指针不同,原始指针允许通过同时拥有不可变和可变指针,或多个可变指针指向同一位置来忽略借用规则。原始指针不保证指向有效的内存,且允许为null。原始指针不实现任何自动清理。通过这些,你可以放弃Rust保证的安全性,以换取更高的性能或与另一种语言或硬件的互操作性。Rust也无法保证这些语言或硬件的安全性。

这里展示了如何从引用创建一个不可变和一个可变的原始指针:

1
2
3
4
let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

注意我们在这段代码中没有包含unsafe关键字。我们可以在安全代码中创建原始指针,我们只是不能在unsafe块之外解引用原始指针。正如你稍后将看到的,我们通过使用as将一个不可变和一个可变引用转换成它们相应的原始指针类型来创建原始指针。因为我们是直接从保证有效的引用创建它们的,我们知道这些特定的原始指针是有效的,但我们不能对所有原始指针做出这样的假设。

为了说明这一点,接下来我们将创建一个我们无法确定其有效性的原始指针:

1
2
let address = 0x012345usize;
let r = address as *const i32;

这段代码展示了如何创建一个指向内存中任意位置的原始指针。尝试使用任意内存是未定义的:那个地址可能有数据,也可能没有。编译器可能会优化代码以至于没有内存访问,或者程序可能会因为段错误而出错。通常没有充分的理由编写这样的代码,但这是可能的。

请记住,我们可以在安全代码中创建原始指针,但我们不能解引用原始指针并读取被指向的数据。我们需要在unsafe块内进行操作。创建指针本身不会造成伤害,只有当我们尝试访问它所指向的值时,我们才可能遇到一个无效的值。

1
2
3
4
5
6
7
8
9
10
let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
println!("r1 is: {}", *r1);
*r2 = 10;
println!("r2 is: {}", *r2);
}

我们创建了指向同一内存位置的const i32和mut i32原始指针,其中存储了变量num的值。如果我们尝试创建一个不可变引用和一个可变引用指向num,代码将无法编译,因为Rust的所有权规则不允许同时存在一个可变引用和任何不可变引用。使用原始指针,我们可以创建指向同一内存位置的可变指针和不可变指针,并通过可变指针更改数据。这可能会导致数据竞争,要小心。

尽管存在这些风险,为什么你还会使用原始指针呢?一个主要的使用场景是与C代码进行交互。另一个情况是在构建安全抽象时,这些抽象是借用检查器无法理解的。

  1. 调用不安全函数

在unsafe块中可以执行的第二种操作是调用不安全函数。不安全函数和方法看起来和普通函数及方法完全一样,但它们在定义的其余部分之前有一个额外的unsafe关键字。在这种情况下,unsafe关键字表示该函数有我们调用它时需要满足的要求,因为Rust无法保证我们已经满足了这些要求。通过在unsafe块内调用一个不安全函数,我们是在告诉Rust我们已经阅读了该函数的文档,并且我们承担起维护函数契约的责任。

这里有一个名为dangerous的不安全函数示例:

1
2
3
4
5
unsafe fn dangerous() {}

unsafe {
dangerous();
}

如果我们尝试在没有unsafe块的情况下调用dangerous,我们会得到一个错误。使用unsafe块,我们向Rust断言我们已经阅读了该函数的文档,我们理解如何正确使用它,并且我们已经验证了我们正在满足函数的契约。

不安全函数的函数体本质上是unsafe块,因此在不安全函数内部执行其他不安全操作时,我们不需要添加另一个unsafe块。仅仅因为一个函数包含不安全代码并不意味着我们需要将整个函数标记为不安全。实际上,将不安全代码封装在安全函数中是一种常见的抽象。

让我们以标准库中的一个函数为例,它需要一些不安全的代码。我们将探讨如何实现它。这个安全方法定义在可变切片上,它接受一个切片,并通过给定的索引参数将其分割成两个切片。我们不能仅使用安全的Rust来实现这个函数。

为了简化,我们将它实现为一个函数,而不是方法,并且只针对i32类型的切片,而不是泛型类型T:

1
2
3
4
5
6
7
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();

assert!(mid <= len);

(&mut slice[..mid], &mut slice[mid..])
}

这个函数首先获取切片的总长度,然后它通过检查给定的索引参数是否小于或等于长度来断言该索引在切片的范围内。这个断言意味着如果我们传递一个大于长度的索引给split_at_mut函数,函数将panic。然后我们返回一个元组中的两个可变切片,一个从原始切片的开始到中间索引,另一个从中间到切片的末尾。

当我们尝试编译这段代码时,我们会得到一个错误。Rust借用检查器无法理解我们正在借用切片的不同部分。它只知道我们两次从同一个切片借用。从本质上讲,借用切片的不同部分是可行的,因为两个切片不会重叠,但Rust还不够智能,无法理解这一点。

当我们知道代码是安全的,Rust却不这么认为时,就是使用不安全代码的时候了。这里展示了如何使用一个不安全的代码块、一个原始指针以及对一些不安全函数的调用来实现split_at_mut函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();

assert!(mid <= len);

unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}

切片是指向某些数据的指针以及切片的长度。我们使用len方法来获取切片的长度,使用as_mut_ptr方法来访问切片的原始指针。因为我们有一个可变的i32值的切片,在这种情况下,as_mut_ptr返回一个类型为*mut i32的原始指针。我们将这个原始指针存储在变量ptr中。

我们保持对mid索引在切片内的断言,然后我们进入不安全代码部分。slice::from_raw_parts_mut函数接受一个原始指针和一个长度,并创建一个切片。我们使用这个函数来创建一个从ptr开始且长度为mid的切片。接着我们调用ptr的add方法并将mid作为参数,以获取一个从mid开始的原始指针。然后我们使用这个指针和mid后剩余的元素数量作为长度来创建一个切片。

slice::from_raw_parts_mut是不安全的,因为它接受一个原始指针,并且必须信任这个指针是有效的。原始指针上的add方法也是不安全的,因为它必须信任偏移位置也是一个有效的指针。因此我们必须在调用slice::from_raw_parts_mut和add的周围加上unsafe块,以便我们能够调用它们。

通过查看代码并添加断言mid必须小于或等于len,我们可以确定在unsafe块内使用的所有原始指针都是指向切片内数据的有效指针。这是一种可接受且适当的使用unsafe的方式。

请注意,我们不需要将split_at_mut函数标记为unsafe。我们可以在安全的Rust中调用这个函数。我们已经创建了一个对unsafe代码的安全抽象,通过实现一个函数,该函数以安全的方式使用unsafe代码,因为它只从该函数可以访问的数据中创建有效的指针。

相比之下,这段代码取任意内存位置并创建一个长度为10000的切片:

1
2
3
4
use std::slice;

let address = 0x01234usize;
let r = slice::from_raw_parts_mut(address as *mut i32, 10000);

我们并不拥有这个任意位置的内存,也没有保证这段代码创建的切片包含有效的i32值。尝试将这些值当作有效的切片来使用会导致未定义行为。

有时你的Rust代码可能需要与用其他语言编写的代码交互。为此,Rust提供了extern关键字,它有助于创建和使用外部函数接口(FFI)。FFI是一种编程语言定义函数的方式,并允许不同的编程语言调用这些函数。

这里展示了如何设置与C标准库中的abs函数的集成:

1
2
3
4
5
6
7
8
9
extern "C" {
fn abs(input: i32) -> i32;
}

fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}

在extern块中声明的函数从Rust代码调用时总是不安全的。原因是其他语言不强制执行Rust的规则和保证,Rust无法检查它们。因此确保安全的责任落在了程序员身上。

在extern “C”块中,我们列出了我们想要调用的来自其他语言的外部函数的名称和签名。”C”部分定义了外部函数使用的应用程序二进制接口(ABI)。ABI定义了在汇编级别如何调用函数。C ABI是最常见的,并遵循C编程语言的ABI。

我们还没有讨论全局变量。Rust是支持全局变量的,但可能会与Rust的所有权规则产生问题。如果两个线程正在访问同一个可变的全局变量,可能会导致数据竞争。

在Rust中,全局变量被称为静态变量。代码展示了一个使用字符串切片作为值的静态变量的声明和使用示例:

1
2
3
4
5
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
println!("name is: {}", HELLO_WORLD);
}

静态变量类似于常量。静态变量只能存储具有’static生命周期的引用,这意味着编译器可以推断出生命周期,我们不需要显式的注解。访问不可变的静态变量是安全的。

常量和不可变静态变量之间的一个微妙区别是静态变量中的值在内存中有一个固定的地址。使用这个值将始终访问相同的数据。另一方面,常量在使用时允许复制它们的数据。

另一个区别是静态变量可以是可变的。访问和修改可变静态变量是不安全的。和普通变量一样,我们使用mut关键字来指定可变性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}

fn main() {
add_to_count(3);

unsafe {
println!("COUNTER: {}", COUNTER);
}
}

这段代码编译并打印出”COUNTER: 3”,正如我们所期望的,因为它是单线程的。如果有多个线程访问COUNTER,很可能会导致数据竞争。对于全局可访问的可变数据,很难确保没有数据竞争。这就是为什么Rust认为可变静态变量是不安全的。

在可能的情况下,最好使用智能指针类型(如标准库中的Mutex),这样编译器就可以检查来自不同线程访问的数据是否安全。

我们可以使用unsafe来实现一个不安全的trait。当一个trait的至少一个方法具有编译器无法验证的不变量时,该trait被认为是不安全的。我们通过在trait前面添加unsafe关键字,并将trait的实现也标记为unsafe来声明一个trait是不安全的。通过使用unsafe impl,我们承诺将维护编译器无法验证的不变量。

举个例子,回想一下我们讨论过的Sync和Send标记trait。如果我们的类型完全由Sync和Send类型组成,编译器会自动实现这些trait。如果我们实现了一个包含非Send或非Sync类型的类型(比如原始指针),并且我们想要将该类型标记为Sync或Send,我们必须使用unsafe。Rust无法验证我们的类型是否支持安全地在多个线程之间发送或从多个线程访问,因此我们需要手动进行这些检查,并用unsafe impl表示。

使用unsafe来执行以上操作并不是禁止的,但是编写不安全代码要更复杂,因为编译器无法帮助维护内存安全。当你有理由使用不安全代码时,你可以这样做,而明确的unsafe注解使得在出现问题时更容易追踪问题的源头。

总的来说,unsafe Rust提供了强大的功能,但也带来了更大的责任。在使用unsafe时,我们需要格外小心,确保我们的代码不会违反Rust的安全保证。同时,unsafe也让Rust能够进行低级系统编程,实现与其他语言的互操作,以及在某些情况下提高性能。正确使用unsafe可以创建安全的抽象,扩展Rust的能力,同时保持整体的安全性。






是谁说 Rust 里不要写 Unsafe 的?

我会按照您的要求整理这段内容,保持原文的完整性和顺序,不省略内容,不做总结。以下是整理后的版本:

是谁说 Rust 里不要写 Unsafe 的?

这个视频我们来聊一下Rust里的unsafe。很多Rust的初学者可能会有这样的一个观点,就是说我们要尽可能避免使用unsafe。还有的人会说unsafe代码越多,我们的程序会越不稳定,越不安全。

首先我们来看看第一个观点,我们是否要尽量避免使用unsafe。其实这个说法有一定它的道理,对于尤其是对于初学者来说,他们可能会遇到一些像可变性啊,生命周期这样的一些问题。那这些问题往往都比较棘手,通过unsafe,可以轻松绕开编译器的各种检查,从而暂时解决他们的问题。那对于这种情况,我们其实确实应该避免使用unsafe,因为它基于的是你没有办法解决现实的一个问题,你的代码可能就是error,但你不知道该怎么解决,所以你通过unsafe来绕开你现在遇到的一个问题。所以我们说新手,尽量要在不写unsafe代码的情况下让你的代码通过编译,这样你可能才会充分对Rust的各种概念有一个比较深入的理解。

然后我们再来看看第二个观点,unsafe越多,程序越不稳定。那这个观点其实就是完全错误的了。unsafe这个关键词,其实有比较强的一个误导性。与其说是unsafe,不如说是”I’m sure it is safe”,就是说当你写unsafe代码的时候,你是在告诉编译器,我比你聪明,你虽然看不懂我的代码,但我可以保证这个代码是没有问题的。所以这个时候的代码的稳定性和它的一个安全性,其实是交给了程序员来保证。如果你能保证你的代码一定是没有问题的,那unsafe代码跟safe代码,在运行时的一个效果就是完全一样的。并不存在说运行到unsafe代码,就一定有这个崩溃的一个风险。那如果真的是这样的话,C语言作为一门unsafe语言,还怎么去开发类似操作系统内核这样的复杂的程序呢?

然后我们再来看看unsafe常见的一些使用场景。首先最常见的就是用来解决一些性能问题。举个例子,当我们需要把一个string的slice转换成这个owned的类型的时候,我们可能会用到这个to_string这个方法。当我们跟踪进去看它的实现的时候,会发现它其实调用到了一个unsafe的函数。那这个原因是因为String,它在定义上是要求我们的字符串是UTF-8编码。那这个限制可以让我们后续其他的比如标准库,其他的操作更加的高效。它可以假设我的这个内容都是基于UTF-8编码的。那由于有这个要求,那我们在构造字符串的时候,它就要去对我们的这个输入的这个buffer去做检查。比如当我们使用String::from_utf8这个函数去构造一个String的时候,我们可以发现,它里面其实是会对传入的这个buffer去做一个Validation。那它会遍历这个字符串,然后看它是不是一个合法的UTF-8的一个编码。当如果说出现问题的时候,就会返回一个error。那其实这就是一个运行时的一个安全保障。

但我们当我们把这个string slice转换成一个String的时候,其实我们已经有一个前置的保证,就是说这个string slice一定也是一个合法的UTF-8内容。那我们再去做重复的一个检查,其实是完全没有必要,会造成一个性能的一个开销。那这个时候你就可以直接把这个字节数组转换成String类型,通过这个from_utf8_unchecked。它可以直接构造一个String,用你传入的这个buffer作为它的底层buffer,来构造这个String。所以这个场景就是用来解决一个性能问题的。

那第二个场景,就是我们现在正在看到的这个函数,from_utf8_unchecked。这个函数是一个unsafe。那如果当我们把一个函数标记为unsafe了,一般就是说这个函数它会暴露一些底层操作。像我们String里面的大部分的函数,它都是safe的。比如说你这个capacity,它获取它的一个这个buffer容量的时候,它是一个safe。push一个char的时候也是一个safe的操作。但唯独就是说这个函数它是unsafe。那其实这个时候是我们想提供一些底层的控制暴露给这个使用方,通常就是用来提升我们的一个性能。那使用方他可以自己评估,如果他可以保证这个安全性,那你就可以使用这个unsafe。

比如说我当我调用这个方法的时候,我前面其实我也可能在某一个场景下,已经对这个buffer做了检查了。我不想让这个String再去做一遍这个UTF-8的一个检查。那我就完全可以自己去调用这个unsafe的方法。由于我自己做过检查,并且我保证这个检查是没有问题,那调用这个函数也一定没有问题。

通常来说我们可以看,就像safe函数它的一个safety的这个注释。那这个注释往往就是说对你的调用的一个要求。如果说你能满足这个要求,整个这个函数的安全性就是可以保证的。所以说一般来说,我们会把这些函数叫做一个escape hatch,就是说当我们遇到一些性能瓶颈,或者说想去做一些底层的操作的时候,并且我们可以保证这个事情的安全性的时候,我们就可以去用这个东西。

那另外一种情况,就是当我们需要跟额外部的一些API去做交互。比如说我们这个libc里面会有一个malloc函数。那这个函数,我们可以发现它是一个unsafe的函数,需要一个unsafe block来使用它。它之所以是unsafe,是因为它使用的这个它提供的这个malloc能力,其实是调用到了我们系统的这个libc的这个库。而这个malloc的封装函数,其实并不是它的实现方,并不是这个crate的作者。所以其实这个crate它只是把我们libc的函数封装成了Rust的接口,但这个库不保证我们libc的实现的可靠性。所以说它其实是把这个风险交给了开发者,那它就不会去帮你承担这个safety的这样一个职责。

当然还有一些场景是我们,比如说你在开发一个crate,然后你写了一些C的代码,然后你想把这部分C的代码封装好,给到额外部开发者去调用。如果说这个C的代码你可以保证安全性,那暴露出去的这个封装函数就完全是可以是safe的。所以说是不是要把一个函数标记为safe或unsafe,只取决于你是否可以保证这个函数的安全性。

所以说这就是Rust的一个优势吧,就是它可以通过unsafe,把这个一些底层的操作去封装成一个安全的API提供给开发者。那通过这个抽象和隔离,可以让这个我们性能和安全性可以达到一个并存。