30天拿下Rust之unsafe代码

概述

在Rust语言的设计哲学中,"安全优先" 是其核心原则之一。然而,在追求极致性能或者与底层硬件进行交互等特定场景下,Rust提供了unsafe关键字。unsafe代码允许开发者暂时脱离Rust的安全限制,直接操作内存和执行低级操作。虽然unsafe代码在某些情况下是必要的,但使用它时必须格外小心,以避免引入难以调试的内存错误。

什么是unsafe代码

在Rust中,unsafe关键字用于标记那些可能破坏Rust的内存安全保证的代码块,使用unsafe关键字编写的代码块或函数被称为unsafe代码。unsafe代码允许程序员执行诸如裸指针操作、类型转换和直接内存访问等低级别操作。由于这些操作可能导致未定义行为或内存安全漏洞,Rust编译器不会对它们进行常规的安全性检查。
unsafe代码主要用于以下三个场景。
性能优化:在某些性能关键的应用中,程序员可能会选择使用unsafe代码来绕过Rust的一些安全检查,以获得更高的性能。
底层系统编程:在操作系统开发、设备驱动或嵌入式系统编程中,可能需要直接操作硬件或使用特定的内存布局,这时就需要使用unsafe代码。
与C语言库交互:当使用Rust调用C语言编写的库时,可能需要执行一些不安全的操作来正确地管理内存和调用约定。
在Rust中,unsafe代码的使用主要涉及以下三个方面:使用裸指针、使用外部函数接口、实现不安全Trait,下面分别进行介绍。

使用裸指针

在Rust中,裸指针是一种可以绕过Rust的常规所有权和借用检查机制的低级工具。它允许程序员直接操作内存地址,从而进行更为底层和灵活的操作。然而,正因为裸指针绕过了Rust的内存安全保证,使用时必须格外小心,以避免引入未定义行为或内存安全问题。
裸指针有两种主要类型:const T(指向常量数据的裸指针)和mut T(指向可变数据的裸指针)。前者用于读取数据,后者用于读取和修改数据。
裸指针通常通过取址操作符&和类型转换来创建。在下面的示例代码中,我们首先创建了一个整数x和一个可变的整数y。然后,我们使用取址操作符&获取它们的地址,并通过类型转换将它们转换为裸指针raw_ptr和mut_raw_ptr 。获取裸指针并不是unsafe代码,解引用裸指针才是unsafe代码。

fn main() {
    let x = 66;
    let raw_ptr: *const i32 = &x as *const i32;

    let mut y = 99;
    let mut_raw_ptr = &mut y as *mut i32;
}

解引用裸指针是通过在裸指针前使用*操作符来完成的,这允许我们读取或修改裸指针指向的值。注意:解引用裸指针时,必须确保指针是有效的,否则会导致未定义行为。
在下面的示例代码中,我们使用unsafe块来解引用裸指针。在unsafe块内,我们打印出raw_ptr指向的值,并将mut_raw_ptr指向的值修改为1024。

fn main() {
    let x = 66;
    let raw_ptr: *const i32 = &x as *const i32;

    let mut y = 99;
    let mut_raw_ptr = &mut y as *mut i32;

    unsafe {
        println!("{}", *raw_ptr);
        *mut_raw_ptr = 1024;
        println!("{}", *mut_raw_ptr);
    }
}

使用外部函数接口

在Rust中,使用unsafe关键字的一个常见场景是调用C语言或其他语言编写的库函数。Rust通过extern块和extern关键字提供了对外部函数的支持,而这些函数的调用通常需要标记为unsafe。这是因为,Rust编译器无法验证这些外部函数的行为是否符合Rust的内存安全规则。
假如我们有下面的C语言库,其Add接口为计算两个整数的和。

// Add.h
#ifdef __cplusplus
extern "C" {
#endif

int Add(int a, int b);

#ifdef __cplusplus
}
#endif


// Add.c
#include "Add.h"

int Add(int a, int b)
{
    return a + b;
}

在下面的示例代码中,我们首先引入了libc库。这是Rust提供的一个包含C语言类型的库,使得我们可以使用与C兼容的类型。然后,我们使用extern "C"块来声明C语言中的Add函数。注意:extern "C"告诉Rust编译器这个函数是用C语言的链接约定来链接的。
在main函数中,我们使用unsafe块来调用这个外部函数。这是必须的,因为Rust编译器无法验证这个C函数是否遵守Rust的内存安全规则。如果C函数违反了这些规则(比如解引用空指针或写入只读内存),那么Rust程序可能会崩溃或产生未定义行为。
最后,编译和运行这个Rust程序需要确保实现Add函数的C库是可用的。我们可能需要编译这个C库为动态链接库或静态库,并在编译Rust程序时链接这个库。另外,我们还需要在Cargo.toml 文件中添加类似下面的依赖性以引入libc库:libc = "0.2"。

use libc::{c_int};

extern "C" {
    fn Add(a: c_int, b: c_int) -> c_int;
}

fn main() {
    unsafe {
        let sum = Add(66, 99);
        println!("{}", sum);
    }
}

实现不安全Trait

在Rust中,可以直接声明一个Trait是不安全的,即整个Trait都带有unsafe修饰符。也可以不声明Trait为不安全的,而在Trait的具体实现中使用unsafe来执行不安全的操作。这意味着,我们可以安全地定义一个Trait,但在其某个或某些具体实现中执行不安全操作。
在下面的示例代码中,UnsafeTrait声明了一个unsafe_method方法。CustomStruct实现了这个Trait,并提供了unsafe_method的一个默认实现,该实现是unsafe的。在main函数中,我们使用unsafe块来调用这个方法,因为我们知道这个调用可能涉及不安全操作。
重要的是,即使unsafe_method是在Trait中定义的,调用它的责任仍然落在调用者身上。调用者必须确保在调用unsafe方法时遵循所有安全准则,比如:确保传递给方法的参数是有效的,并处理任何可能由unsafe操作引起的错误或未定义行为。

trait UnsafeTrait {
    unsafe fn unsafe_method(&self) -> Result<(), String>;
}

struct CustomStruct;

impl UnsafeTrait for CustomStruct {
    unsafe fn unsafe_method(&self) -> Result<(), String> {
        // 在这里执行一些可能不安全的操作
        Ok(())
    }
}

fn main() {
    let my_struct = CustomStruct;
    unsafe {
        match my_struct.unsafe_method() {
            Ok(()) => println!("success"),
            Err(e) => println!("failed: {}", e),
        }
    }
}

通常,应该尽量避免在Trait中使用unsafe,除非确实需要执行一些低级的、不安全的操作,并且调用者能够清楚地理解并处理这些不安全操作可能带来的风险。在大多数情况下,更好的做法是:使用安全的Rust特性来实现相关的需求。

unsafe代码的安全抽象

unsafe代码的安全抽象是一种设计模式,它允许开发者在不安全代码和安全代码之间建立清晰的边界。这种抽象通过封装不安全操作在安全的接口之后来实现,使得库的使用者能够在不了解或不关心内部实现细节的情况下安全地使用库的功能。这种设计模式的关键在于:将不安全代码限制在尽可能小的范围内,并通过安全的接口暴露给使用者。这样,库的使用者可以依赖这些安全的接口,而无需担心底层可能的不安全操作。
在下面的示例代码中,unsafe_operation函数执行一些不安全操作。然而,它并没有直接公开给库的使用者,而是被封装在safe_operation函数中。safe_operation函数是一个安全的接口,它内部使用unsafe块来调用unsafe_operation,但在调用前后可以添加额外的安全检查或清理工作。这样,库的使用者只需要调用safe_operation,而无需关心其内部是否使用了unsafe。

unsafe fn unsafe_operation() {
    // ...
}

pub fn safe_operation() {
    unsafe {
        unsafe_operation();
    }
    // 可以在这里添加额外的安全检查或清理工作
}
  
fn main() {
    safe_operation();
}

通过安全抽象这种方式,库的设计者可以确保库的使用者不会误用不安全操作,同时仍然能够利用不安全代码提供的性能优势或底层功能。在构建大型Rust项目或库时,将不安全代码限制在最小的必要范围内,并通过安全的接口暴露功能是非常重要的。这有助于减少错误和漏洞的风险,同时提高代码的可维护性和可理解性。

注意事项

虽然unsafe并非完全不受控制,但它确实把内存安全的责任交还给了程序员。在编写unsafe代码时,我们需要特别注意以下几点。
1、最小化unsafe代码的使用。尽量将unsafe代码的使用限制在必要的范围内,并尽量避免在库或模块的公共API中使用它。
2、仔细审查unsafe代码。对unsafe代码进行严格的代码审查和测试,以确保它不会引入内存安全漏洞。
3、文档化unsafe代码。为unsafe代码提供清晰的文档说明,解释为什么需要使用它,以及使用它时需要注意的事项。
4、使用Rust的安全抽象。尽可能利用Rust提供的所有权模型、生命周期和借用检查器等安全抽象来减少unsafe代码的使用。

总结

Rust的unsafe代码是强大且必要的工具,它让Rust能够在提供高级抽象的同时,依然保留对底层资源的精细控制能力。然而,unsafe代码也是一个潜在的危险源。使用unsafe代码需要开发者具备足够的经验和谨慎,始终坚守Rust的内存和类型安全准则。只有这样,我们才能充分利用Rust的优势,构建出既高效又安全的系统级软件。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容