Rust突破编译器限制构造可修改的全局变量

 更新时间:2023年10月15日 11:10:00   作者:garfileo  
这篇文章主要为大家介绍了Rust突破编译器限制构造可修改的全局变量示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

问题

在前面一些章节里,在使用正则表达式对文本进行分割时,皆使用局部变量存储正则表达式,且皆为硬代码——在程序运行时无法修改正则表达式。本章尝试构造一个可在运行时被修改的全局变量用以表达正则表达式。

失败的全局原始指针

倘若将原始指针作为全局变量,在程序运行时,可以令其指向与其类型相匹配的任何一个值,这是我想要的全局变量。于是,试着写出以下代码:

use regex::Regex;
use std::ptr::null_mut;
let a: *mut Regex = null_mut();

fn main() {
    a = Box::into_raw(Box::new(Regex::new(" *@ *")));
    let v = (*a).unwrap().split("num@ 123@456  @ 789");
    for i in v {
        println!("{}", i);
    }
}

Rust 编译器编译上述代码时会报错,建议使用 const 或 static 代替全局变量 a 的定义语句中的 let,亦即 Rust 语言不允许使用 let 定义全局变量。const 修饰的全局变量,其值不可修改。static 修饰的全局变量,其值可修改。故而,我将变量 a 的定义修改为

static a: *mut Regex = null_mut();

Rust 编译器依然报错,称 *mut regex::Regex 类型的值不能被不同的线程安全共享,虽不甚知其意,但也应知此路不通了。

也许在素有经验的 Rust 程序员看来,上述代码会令他一言难尽,但是如果我说通过以上代码可以看出 Rust 语言并不希望程序员使用全局变量,料想不会引起他的反对。Rust 不希望什么,那是它的事,而我却需要它。现在的问题是,无法构造全局原始指针。Rust 编译器给出的建议是,如果想让 *mut regex::Regex 类型的指针作为全局变量,前提是需要为该类型实现 Sync 特性。这个建议对于目前的我来说是超纲的,所以我完全可以认为,在 Rust 语言中不允许出现全局原始指针。

Option<T> 于事无补

在表示空值方面,Option<T> 类型可以代替原始指针,用该类型封装原始指针是否能作为全局变量呢?试试看:

static foo: Option<*mut i32> = None;
fn main() {
    let a = 3;
    foo = Some(&a as *mut i32);
    println!("{:?}", foo);
}

答案是否定的。Rust 编译器依然称:

`*mut i32` cannot be shared between threads safely

并建议

shared static variables must have a type that implements `Sync`

此路依然不通。

结构体屏障

无论是直接用原始指针,还是用 Option<T> 封装原始指针,在构造全局变量时,都会导致原始指针直接暴露在 Rust 编译器面前,而编译器坚持认为,所有的全局变量类型都应该实现 Sync 特性。现在,换一个思路,倘若将原始指针类型封装在结构体中,是否可以骗过编译器呢?

以下代码将 *mut i32 类型的指针封装在一个结构体类型中,并使用该结构体类型构造全局变量:

#[derive(Debug)]
struct Foo {
    data: *mut i32
}
static mut A: Foo = Foo{data: std::ptr::null_mut()};
fn main() {
    unsafe {
        println!("{:?}", A);
    }
}

上述程序可以通过编译,其输出为

Foo { data: 0x0 }

以下代码尝试能否修改 A.data 的值:

let mut a = 3;
unsafe {
    A.data = &mut a as *mut i32;
    println!("{:?}", A);
    println!("{}", *A.data);
}

依然能通过编译,其输出结果与以下结果类似:

Foo { data: 0x7fff64cdecb4 }
3

这样骗编译器,好么?我不知道。Rust 标准库在 std::marker::Sync 的文档中提到,所有的基本类型,复合类型(元组、结构体和枚举),引用,Vec<T>Box<T 以及大多数集合类型等皆实现了 Sync 特性,所以上述手法并不能称为「骗」。

回到本章开始的问题,现在可写出以下代码:

use regex::Regex;
use std::ptr::null_mut;
#[derive(Debug)]
struct Foo {
    data: *mut Regex
}
static mut A: Foo = Foo{data: null_mut()};
fn main() {
    unsafe {
        A = Foo {data: Box::into_raw(Box::new(Regex::new(" *@ *").unwrap()))};
        let v = (*A.data).split("num@ 123@456  @ 789");
        for i in v {
            println!("{}", i);
        }
        let _ = Box::from_raw(A.data);
    }
}

注意,上述代码中的 let _ = ... 表示不关心右侧函数调用的返回值,但是该行代码可将 A.data 指向的内存空间归还于 Rust 的智能指针管理系统,从而实现自动释放。

制造内存泄漏

上述基于原始指针的全局变量构造方法似乎并不为 Rust 开发者欣赏,因为在他们眼里,任何一个原始指针都像一个不知道什么时候会被一脚踩上去的地雷,他们更喜欢是引用。

下面尝试使用引用构造全局变量。由于引用不具备空值,所以必须使用 Option<T> 进行封装,例如

use regex::Regex;
static mut A: Option<&Regex> = None;

fn main() {
    unsafe {
        let re = Regex::new(" *@ *").unwrap();
        A = Some(&re);
        // ... 待补充
    }
}

Rust 编译器对上述代码给出的错误信息是,re 被一个全局变量借用,但是前者的寿命短于后者,亦即当后者还存在时,前者已经死亡,导致后者引用失效。在 C 语言中,这种错误就是鼎鼎有名的「悬垂指针」错误,Rust 编译器会尽自己最大能力去阻止此类错误。

不过,Rust 标准库给我们留了一个后门,使用 Box<T> 的 leak 方法可将位于堆空间的值的寿命提升为全局变量级别的寿命:

unsafe {
    let re = Box::new(Regex::new(" *@ *").unwrap());
    A = Some(Box::leak(re));
    let v = A.unwrap().split("num@ 123@456  @ 789");
    for i in v {
        println!("{}", i);
    }
}

需要注意的是,Box::leak 名副其实,会导致内存泄漏,因为堆空间的值其寿命经 Box::leak 提升后,与程序本身相同,无法回收。Rust 官方说,如果你介意这样的内存泄漏,那就需要考虑走原始指针路线。

延迟初始化

对于支持运行时修改的全局变量,还有一类方法是将全局变量的初始化推迟在程序运行时,但该类方法要么依赖第三方库(crate),例如 lazy_static,要么是标准库目前尚未稳定的功能 OnceCell,此外该类方法只能对全局变量完成一次赋值。这些方法,rzeo 并不打算使用,故而略过。

值的所有权转移

基于值的所有权转移也能实现在程序的运行时修改全局变量的值。例如

use regex::Regex;
static mut A: Option<Regex> = None;

fn main() {
    unsafe {
        let re = Regex::new(" *@ *").unwrap();
        A = Some(re);
        let v = A.unwrap().split("num@ 123@456  @ 789");
        for i in v {
            println!("{}", i);
        }
    }
}

不过,上述代码无法通过编译,原因是 Option<T> 的实例方法 unwrap 需要转移实例的所有权——消耗一个临时变量,但是上述代码中的 Option<T> 的实例 A 是全局变量,与程序同寿,其所有权无法转移。有两种方法可规避该错误,一种是

unsafe {
    let re = Regex::new(" *@ *").unwrap();
    A = Some(re);
    match A {
        Some(ref b) => {
            let v = b.split("num@ 123@456  @ 789");
            for i in v {
                println!("{}", i);
            }
        },
        None => panic!("...")
    }
}

另一种是使用 Option<T> 的 as_ref 方法,将类型 &Option<T> 转换为类型 Option<&T>,然后使用 Option<&T> 的 unwrap 方法:

unsafe {
    let re = Regex::new(" *@ *").unwrap();
    A = Some(re);
    let v = A.as_ref().unwrap().split("num@ 123@456  @ 789");
    for i in v {
        println!("{}", i);
    }
}

不妨将 as_ref 方法视为上述模式匹配代码的简化。

小结

全局变量是构成程序的不安全因素之一,但它并非洪水猛兽,只要保证程序在任一时刻全局变量不会被多个线程同时修改即可。如果全局变量给程序带来了灾难,这往往意味着是程序的设计出现了严重问题。我认为 Rust 对全局变量的限制太过于严厉,特别是在禁止直接将原始指针作为全局变量这一方面,毕竟即使不使用原始指针,对全局变量的修改在 Rust 语言看来,也是不安全的。既然都不安全,何必五十步笑百步。

以上就是Rust突破编译器限制构造可修改的全局变量的详细内容,更多关于Rust全局变量的资料请关注脚本之家其它相关文章!

相关文章

  • Rust指南之生命周期机制详解

    Rust指南之生命周期机制详解

    Rust 生命周期机制是与所有权机制同等重要的资源管理机制,之所以引入这个概念主要是应对复杂类型系统中资源管理的问题,这篇文章主要介绍了Rust指南之生命周期机制详解,需要的朋友可以参考下
    2022-10-10
  • 关于rust的模块引入问题

    关于rust的模块引入问题

    Rust 语言是一种高效、可靠的通用高级语言,它的执行效率也是令人称赞的,是一种少有的兼顾开发效率和执行效率的语言,这篇文章主要介绍了rust的模块引入相关知识,需要的朋友可以参考下
    2022-10-10
  • Rust 中 Mutex 的基本用法

    Rust 中 Mutex 的基本用法

    Rust 标准库中的 Mutex 结构体位于 std::sync::Mutex 中,它提供了线程安全的数据访问,Mutex 保证了在同一时间只有一个线程可以访问被锁定的数据,这篇文章主要介绍了Rust 中 Mutex 的基本用法,需要的朋友可以参考下
    2024-05-05
  • 探索 Rust 中实用的错误处理技巧

    探索 Rust 中实用的错误处理技巧

    探索Rust中实用的错误处理技巧!Rust是一门静态类型系统安全且高效的编程语言,但使用过程中难免会遇到各种错误,学会如何正确处理这些错误至关重要,本指南将为您提供一些实用的错误处理技巧,帮助您更好地编写健壮的代码,需要的朋友可以参考下
    2024-01-01
  • Rust之智能指针的用法

    Rust之智能指针的用法

    在Rust中,智能指针是管理内存所有权和生命周期的核心工具,本文主要介绍了Rust之智能指针的用法,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • Rust实现冒泡排序算法示例详解

    Rust实现冒泡排序算法示例详解

    这篇文章主要为大家介绍了Rust实现冒泡排序算法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • Rust语言之结构体和枚举的用途与高级功能详解

    Rust语言之结构体和枚举的用途与高级功能详解

    Rust 是一门注重安全性和性能的现代编程语言,其中结构体和枚举是其强大的数据类型之一,了解结构体和枚举的概念及其高级功能,将使你能够更加灵活和高效地处理数据,本文将深入探讨 Rust 中的结构体和枚举,并介绍它们的用途和高级功能
    2023-10-10
  • Rust版本号的使用方法详解

    Rust版本号的使用方法详解

    在 Rust 项目中,版本号的使用遵循语义版本控制(Semantic Versioning)原则,确保版本号的变化能准确反映代码的变更情况,本文给大家详细解释了Rust版本号用法,需要的朋友可以参考下
    2024-01-01
  • Rust中字符串类型String的46种常用方法分享

    Rust中字符串类型String的46种常用方法分享

    Rust主要有两种类型的字符串:&str和String,本文主要为大家介绍的是String类型的字符串以及它常用的46种方法,感兴趣的小伙伴可以了解一下
    2023-06-06
  • 解析Rust struct 中的生命周期

    解析Rust struct 中的生命周期

    rust 的生命周期保证了内存的安全性,同时也增加了开发者的心智负担。是在上线之前多费心思写代码,还是在上线以后忙忙活活查问题,这是个 trade off 问题,这篇文章主要介绍了Rust struct 中的生命周期,需要的朋友可以参考下
    2022-10-10

最新评论