• Rust中FFI编程知识点整理总结


    Rust语言对FFI的支持

    Rust 语言主要在关键字和标准库两个方面对 FFI 提供了支持,具体如下:
    关键字 extern
    属性 #[no_mangle]
    外部块 ExternBlock 及其属性 link 和 link_name
    标准库
    std:os:raw 模块:例如c_char。
    std:ffi 模块:传递 UTF-8 字符串时,CString和CStr很有用。

    libc-crate库

    你可以使用 libc::foo 这种形式访问这个库中的任何导出内容。
    在Rust里,只能创建子线程,如果想创建子进程,就需要用到libc库

    fn main() {    
    unsafe {        
    let pid = libc::fork();                                                                                                               if pid > 0 {println!("Hello, I am parent thread: {}", libc::getpid());}   
    else if pid == 0 {println!("Hello, I am child thread: {}", libc::getpid());println!("My parent thread: {}", libc::getppid());        }        
    else {println!("Fork creation failed!");}}}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.libc 的所有函数调用,都必须放进 unsafe 块中。因为它的所有调用都是 unsafe 的;
    2.std 的线程操作封装,好用,形象。libc 的进程操作,与 C 语言系统编程一样,完全是另外一套思路和编程风格;
    3.std 的线程操作虽然简洁,但是也缺少更细颗粒度的控制。而 libc 可以对进程的操作(及后面对子进程的功能扩充,父进程中的信号管理等),做到完全的控制,更加灵活,功能强大;
    4.std 本身无法实现进程 fork 的功能。
    因为我 Rust 的封装是 zero cost (零成本)的。零成本抽象赋予了 Rust 系统编程的能力。

    libc 与 std::os:😗::raw,这里面有的用法是一样的,没有任何问题。简单的和C交互可以用os:raw里面的,而一旦产生了系统调用或者 Unix 环境编程,那么就得引入 libc 库来操作。

    cbindgen 工具的介绍和使用

    这个工具就是将写好的Rust代码配置一下,然后会自动生成接口代码头文件等等。其实,FFI封装、转换,熟悉了之后,知识点就那些,模式也比较固定,如果接口量很大,那就需要大量重复的 coding。量一大,人手动绑定出错的机率也大。所以这种辅助工具的意义就显露出来了。基于辅助工具生成的代码,如不完美,再适当手动修一修,几下就能搞定,大大提高生产效率。

    Rust指针

    在Rust中,存在三种类型的指针:

    1.Rust自带的指针类型:引用—安全的指针

    &T:它是对类型T的不可变引用
    &mut T:它是对类型T的可变引用

    2. 原始指针

    众所周知,Rust语言的指针是一种安全的指针,它会遵循一定的规则,比如ownership规则,会确保不出现悬挂指针。但是当我们需要写一些底层框架的时候,往往需要绕过这些规则,自由的控制指针,这时候我们就可以使用原始指针。
    *const T:表示指向类型T的不可变原始指针。它是Copy类型。这类似于&T,只是它可以为空值。
    *mut T:一个指向T的可变原始指针,它不支持Copy特征(non-Copy)。
    以下可以定义Rust的原始指针:

    fn main() {
        let mut num = 5;
        let r1 = &num as *const i32;
        let r2 = &mut num as *mut i32;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    3.智能指针

    管理原始指针非常不安全,开发者在使用它们时需要注意很多细节。不恰当地使用它们可能会以非常隐蔽的方式导致诸如内存泄漏、引用挂起,以及大型代码库中的双重释放等问题。为了解决这些问题,我们可以使用C++中广泛采用的智能指针。
    智能指针的两个特性:Drop和Deref
    Drop:这是多次提及的特征,它可以自动释放相关值超出作用域后占用的资源。Drop特征类似于你在其他语言中遇到的被称为对象析构函数的东西。它包含一个drop方法,当对象超出作用域时就会被调用。该方法将&mut self作为参数。使用drop释放值是以LIFO的方式进行的。也就是说,无论最后构建的是什么,都首先会被销毁。drop方法是你为自己的结构体放置清理代码的理想场所。例如使用引用计数值或GC时,它尤其方便。当我们实例化任何Drop实现值时(任意堆分配类型),Rust编译器会在编译后的代码中每个作用域结束的位置插入drop方法调用。因此,我们不需要在这些实例上手动调用drop方法。
    Deref:为了提供与普通指针类似的行为,也就是说,为了能够解引用被指向类型的调用方法,智能指针类型通常会实现Deref特征,这允许用户对这些类型使用解引用运算符*。虽然Deref只为你提供了只读权限,但是还有DerefMut,它可以为你提供对底层类型的可变引用。

    智能指针的种类:
    标准库中的智能指针有如下几种。
    Box:它提供了最简单的堆资源分配方式。Box类型拥有其中的值,并且可用于保存结构体中的值,或者从函数返回它们。
    Rc:它用于引用计数。每当获取新引用时,计数器会执行递增操作,并在用户释放引用时对计数器执行递减操作。当计数器的值为零时,该值将被移除。
    Arc:它用于原子引用计数。这与之前的类型类似,但具有原子性以保证多线程的安全性。
    Cell:它为我们提供实现了Copy特征的类型的内部可变性。换句话说,我们有可能获得多个可变引用。
    RefCell:它为我们提供了类型的内部可变性,并且不需要实现Copy特征。它用于运行时的锁定以确保安全性。

    引用计数指针:
    所有权规则只允许某个给定作用域中存在一个所有者。但是,在某些情况下你需要与多个变量共享类型。例如在GUI库中,每个子窗体小部件都需要具有对其父容器窗口小部件的引用,以便基于用户的resize事件来调整子窗口的布局。虽然有时生命周期允许你将父节点存储为&'a Parent,但是它通常受到’a值生命周期的限制,一旦作用域结束,你的引用将失效。在这种情况下,我们需要更灵活的方法,并且需要使用引用计数类型。程序中的这些智能指针类型会提供值的共享所有权。

    引用计数类型支持某个粒度级别的垃圾回收。在这种方法中,智能指针类型允许用户对包装值进行多次引用。在内部,智能指针使用引用计数器(这里是refcount)来统计已发放的并且活动的引用数量,不过它只是一个整数值。当引用包装的智能指针值的变量超出作用域时,refcount的值就会递减。一旦该对象的所有引用都消失,refcount的值也会变成0,之后该值会被销毁。这就是引用计数指针的常见工作模式。

    Rust为我们提供了两种引用计数指针类型。
    Rc:这主要用于单线程环境。
    Arc:这主要用于多线程环境。

    Rust和C交互时的各种指针变换

    1.pub extern “C” fn sum_of_array(array: *const u32, len: usize) -> u32
    slice::from_raw_parts(array,len)
    C端传来的数组(指针类型),进到Rust这边进行强制类型转换,变成非可变原始指针类型。函数slice::from_raw_parts(array,len)就是对原始指针进行转换为Rust切片类型,切片就是一个指针+一个长度即可。

    2.CStr::from_ptr(raw_string):CStr就是C端产生数据,Rust端使用,
    只是借用,常用于打印。raw_string是直接从C接过来的可变原始指针。
    使用std::ffi::CStr提供的from_ptr方法包装 C 的字符串指针,它基于空字符’\0’来计算字符串的长度,并可以通过它将外部 C 字符串转换为 Rust 的 &str和String

    use std::ffi::CStr;
    use libc::c_char;
    extern {
    fn char_func() -> *mut c_char;
    }
    
    fn get_string() -> String {
    unsafe {
    let raw_string: *mut c_char = char_func();
    let cstr = CStr::from_ptr(raw_string);
    cstr.to_string_lossy().into_owned()
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.CStr::from_ptr(s).to_string_lossy().into_owned():注意to_string_lossy()的使用:因为在rust中一切字符都是采用utf8表示的而c不是,
    因此如果要将c的字符串转换到rust字符串的话,需要检查是否都为有效utf-8字节。

    4.CString::new(“Hello, world!”).as_ptr():Cstring是Rust端产生数据,C端进行使用。
    as_ptr()就是将RustCString指针类型转化为C的原始指针类型。

    5.CString::new(“Hello world!”).into_raw()
    使用std::ffi::CString提供的一对方法into_raw和from_raw可以进行原始指针转换,由于将字符串的所有权转移给了调用者,所以调用者必须将字符串返回给 Rust,以便正确地释放内存。
    into_raw()和.as_ptr()的作用类似,都是变成原始指针传给C端。
    6.CString::from_raw(s)
    一般在释放内存的时候使用,C端用完需要Rust端来释放。

    7.Box::into_raw(Box::new(new_stu)):其实这里是智能指针和两端堆栈申请有关,into_raw()就是将Rust智能指针变成原始指针。
    8.Box::from_raw(p_stu):from_raw():就是将C端传来的p_stu变成Rust智能指针。

    数组类型传递

    C代码:

      uint32_t sum = sum_of_array(numbers, length);
    
    • 1

    Rust代码:

    pub extern "C" fn sum_of_array(array: *const u32, len: usize) -> u32 {
        let array = unsafe {
            assert!(!array.is_null());
            slice::from_raw_parts(array, len)
        };
       array.iter().sum()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里的参数传递一目了然,array一开始是C过来的指针类型,通过slice::from_raw_parts(array,len)之后,变成一个Rust切片类型,后面用iter进行求和。切片类型就是一个指针和一组数据合在一起组成。

    字符串类型

    对于C语言来说,字符串有两种,一种是共享的只读字符串 char * ,不能修改。另一种是动态分配的可变字符串 char [],可以修改。
    而在Rust里面,字符串是由字符的 UTF-8 编码组成的字节序列。表示的类型有很多种。
    字符串则比较复杂,Rust 中的字符串,是一组u8组成的 UTF-8 编码的字节序列,字符串内部允许NULL字节;但在 C 中,字符串只是指向一个char的指针,用一个NULL字节作为终止。
    我们需要做一些特殊的转换,在 Rust FFI 中使用std::ffi::CStr,它表示一个NULL字节作为终止的字节数组,可以通过 UTF-8 验证转换成 Rust 中的&str。
    CStr:表示以空字符终止的 C 字符串或字节数组的借用,属于引用类型。一般用于和 C 语言交互,由 C 分配并被 Rust 借用的字符串。
    CString:表示拥有所有权的,中间没有空字节,以空字符终止的字符串类型。一般用于和 C 语言交互时,由 Rust 分配并传递给 C 的字符串。
    下面这段代码,在这里get_string使用CStr::from_ptr从C的char*获取一个字符串,并且转化成了一个String。

    fn get_string() -> String {
    unsafe {
    let raw_string: *mut c_char = char_func();
    let cstr = CStr::from_ptr(raw_string);
    cstr.to_string_lossy().into_owned()
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    和CStr表示从C中来,rust不拥有归属权的字符串相反,CString表示由rust分配,Rust拥有所有权,可以进行修改,用以传给C程序的字符串。

    use std::ffi::CString;
    use std::os::raw::c_char;
    extern {
    fn my_printer(s: *const c_char);
    }
    
    let c_to_print = CString::new("Hello, world!").unwrap();
    unsafe {
    my_printer(c_to_print.as_ptr()); // 使用 as_ptr 将CString转化成char指针传给c函数
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    两端分配堆栈,另一端填充打印
  • 相关阅读:
    【python与数据结构】(leetcode算法预备知识)
    8-图文打造LeeCode算法宝典-最小栈与LRU缓存机制算法题解
    【Transformers】第 2 章:文本分类
    能跑通的mmdet3d版本
    MySQL 视图View的SQL语法和更新(视图篇 二)
    线性表但是是Java中数组实用使用
    【数据库编程-SQLite3(四)】基本常用操作
    【多线程案例】设计模式-单例模式
    Pytorch 基于AlexNet的服饰识别(使用Fashion-MNIST数据集)
    MyBatisPlus快速入门
  • 原文地址:https://blog.csdn.net/phthon1997/article/details/126764866