Rust中FFI编程知识点整理总结(推荐)

2022-11-13 17:11:05 知识点 编程 整理

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

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

到此这篇关于Rust中FFI编程知识点整理总结的文章就介绍到这了,更多相关Rust中FFI编程内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章