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.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;
}

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()
}
}

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);

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()
}

这里的参数传递一目了然,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()
}
}

和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函数
}

两端分配堆栈,另一端填充打印

原文地址:https://blog.csdn.net/phthon1997/article/details/126764866