Rust FFI编程中的内存分配
熟悉rust的朋友,应该都知道rust语言本身是没用垃圾回收的概念的。为了在编译期间就能合理的规划和使用内存,rust的变量都有自己的生命周期,在发生借用和转移之后就会被释放。因此rust并不需要java(分代和并发)和go(三色标记和并行)等这样的垃圾集中回收机制,或者像c/c++一样的主动分配释放的机制。
因此在rust FFI编程中就会遇到一个有意思的问题,如何像C一样将一块分配好的内存传递给另一个函数调用方。让rust的变量所有者在离开了这个作用域后这个值不被清理,而是像c一样来主动释放?
那么通过rust来调用libc的malloc和free将无疑是一个很好的解决方案。让rust像胶水层一样粘连在c程序和rust(libc)之间。这样就不用担心rust的主动清理问题了。
一. 关于Rust FFI编程
Rust 的 FFI(Foreign Function Interface)功能,允许您使用 Rust 编写 C 兼容的静态和动态链接库,使其可以和C或者其他语言的应用程序进行交互。
从 Rust 的角度来看,FFI 是通过在 Rust 代码中定义 C 兼容的接口来实现的,而从 C 应用程序的角度来看,则需要使用与 Rust 兼容的头文件和库文件。
1. rust FFI编程示例
接下来我将给出一个简单的例子,例子中rust实现了一个求和功能的函数,而这个函数将被编译成C语言能够调用的动态链接库来供C程序调用。
实现Rust 的 FFI能力,主要有以下步骤:
- 使用 extern 和 #[no_mangle] 标记 Rust 函数(lib.rs中),以便可以从其他语言调用该函数:
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
- 在 Cargo.toml 文件中,设置 crate-type 选项为 cdylib:
[lib]
name = "add"
crate-type = ["cdylib"]
之后通过 cargo build 命令来编译我们的rust程序,这样就能够通过C语言使用动态链接库来调用我的add方法了。
- 在 C 代码中包含 Rust FFI 公开的头文件,并调用 Rust 函数。
#include "add.h"
int main() {
int sum = add(2, 3);
printf("sum = %d\n", sum);
return 0;
}
- 编译 C 代码并链接 Rust FFI 动态链接库
gcc main.c -o main -L/target/release/ -l add
二. rust FFI中内存分配
下面让我来看一看,如何在rust中实现内存分配,并将内存地址传递给C程序,让C程序来调用,并释放。
示例程序中,将通过rust来规划一块pools的结构体数组空间,并将结构体数组的地址传递给c程序,c程序在使用完这块空间后调用rust提供的函数指针将其释放(这里其实用c来释放也行,但是本着谁的孩子谁来带的原则,这里还是提供了一个内存释放函数,让rust来分配和释放这块空间)。
以下是示例程序的目录结构:
.
├── Cargo.lock
├── Cargo.toml
├── include
│ └── test.h
├── src
│ └── lib.rs
└── test.c
- 动态链接口的头文件 include/test.h (这个文件更像是C和rust在ffi之间的协议一样)
#ifndef TEST_H
#define TEST_H
//Pool 结构体
typedef struct {
int id;
char name[20];
} Pool;
// 此方法,接受三个内存地址:pools,分配好的结构体数组空间的指针地址,len:结构体数组的长度, mem_free: 内存释放函数指针地址
void create_pools(Pool **pools,unsigned int *len,void (**mem_free)(void **));
#endif /*TEST*/
- 导入rust需要使用的库,并配置Cargo.toml
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
libc = "0.2.0"
- 在明确头文件后,我们就可以根据头文件来实现我门的rust ffi程序了。src/lib.rs
use std::os::raw;
use libc::{free, malloc};
use std::mem::size_of;
// Pool 结构体
pub struct Pool {
pub id: i32,
pub name: [raw::c_char; 20usize],
}
pub extern "C" fn mem_free(ptr: *mut *mut raw::c_void) {
println!("I‘m free func");
if ptr.is_null() {
println!("it is null, nothing to free");
return;
}
unsafe { free(*ptr) } //调用libc库的free方法,释放内存空间
}
#[no_mangle]
pub extern "C" fn create_pools(
pools: *mut *mut Pool,
len: *mut raw::c_uint,
mem_free: *mut ::std::option::Option<unsafe extern "C" fn(arg1: *mut *mut raw::c_void)>,
) {
// 分配内存的大小
let size = 3 * size_of::<Pool>();
// 使用 malloc 分配内存
unsafe {
let ptr = malloc(size);
// 向分配的内存写入数据
let array = std::slice::from_raw_parts_mut(ptr as *mut Pool, 3) ;
for i in 0..3 {
let name_str = format!("demo_{}", i);
let c_string = std::ffi::CString::new(name_str).unwrap();
let names = c_string.as_bytes_with_nul();
let mut name_buffer: [raw::c_char; 20] = [0; 20];
for (i, byte) in names.iter().enumerate() {
name_buffer[i] = *byte as raw::c_char;
}
array[i] = Pool {
id: i as i32,
name: name_buffer,
};
}
// 返回指向分配的内存的指针,pools的长度len,和释放内存函数指针回填。
*pools = ptr as *mut Pool;
*len = 3;
*mem_free = Some(mem_free);
};
}
- 在C程序的测试示例:test.C
#include "include/test.h"
#include <stdio.h>
int main(void){
Pool *pools = NULL;
unsigned int len = 0;
void (*free_func)(void **ptr) = NULL;
//调用rust的内存分配函数
create_pools(&pools,&len,&free_func);
int i;
for(i=0;i<3;i++){
printf("name:%s id:%d \n",pools[i].name,pools[i].id);
}
//调用rust的内存释放函数
free_func(&pools);
printf("demo_0: name:%s id:%d \n",pools[0].name,pools[0].id);
}
- 测试验证
[root@local demo]# cargo build
[root@local demo]# gcc -Wall -g -O0 test.c -I. -L target/debug/ -l demo
[root@local demo]# LD_LIBRARY_PATH=target/debug ./a.out
name:demo_0 id:0
name:demo_1 id:1
name:demo_2 id:2
I‘m free func
demo_0: name: id:0
cargo build 后将在 ./target/debug 目录下生成一个libdemo.so动态链接库。
gcc -Wall -g -O0 test.c -I. -L target/debug/ -l demo 链接编译
从示例的输出中可以看到,在释放完成后访问对应的pools成员变量将得到空值。