本篇,我们说明 Rust 调用 C 的另外一种场景:内存在 Rust 这边分配,在 C 中进行填充。
我们依旧使用上一篇中设计的例子,稍作修改:在 C 端增加一个填充数据的函数fill_data
,函数签名:
Student* fill_data(Student *stu)
。整个示例流程如下:
- 在 C 端,有个结构体,字段有整型,字符串,浮点型;
- 在 C 端,两个函数,打印结构体数据的
print_data
,填充结构体数据的fill_data
; - 在 Rust 中,分配内存资源,初始化,并打印;
- 在 Rust 中,调用 C 中的
fill_data
填充结构体,并调用 C 中的print_data
打印; - 在 Rust 中,再次打印 C 填充后的结构体数据。
话不多说,直接上代码。我们创建了一个名为 example_09 的 cargo 工程。位于 csrc 目录的 C 端代码如下:
// filename: cfoo.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
typedef struct Student
{
int num;
int total;
char name[20];
float scores[3];
} Student;
void print_data(Student *stu)
{
printf("C side print: %d %s %d %.2f %.2f %.2f\n",
stu->num,
stu->name,
stu->total,
stu->scores[0],
stu->scores[1],
stu->scores[2]);
}
void fill_data(Student *stu)
{
stu->num = 2;
stu->total = 100;
strcpy(stu->name, "Bob");
stu->scores[0] = 60.6;
stu->scores[1] = 70.7;
stu->scores[2] = 80.8;
}
使用
gcc -fPIC -shared -o libcfoo.so cfoo.c
编译生成 libcfoo.so
。
Rust 端的代码在 main.rs
中如下:
use std::os::raw::{c_char, c_float, c_int};
#[repr(C)]
#[derive(Debug)]
pub struct CStudent {
pub num: c_int,
pub total: c_int,
pub name: [c_char; 20],
pub scores: [c_float; 3],
}
// Default constructor
impl Default for CStudent {
fn default() -> Self {
CStudent {
num: 0 as c_int,
total: 0 as c_int,
name: [0 as c_char; 20],
scores: [0.0 as c_float; 3],
}
}
}
#[link(name = "cfoo")]
extern "C" {
fn print_data(p_stu: *mut CStudent);
fn fill_data(p_stu: *mut CStudent);
}
fn main() {
// Initialization of allocated memory
let new_stu: CStudent = Default::default();
println!("rust side print new_stu: {:?}", new_stu);
let box_new_stu = Box::new(new_stu);
let p_stu = Box::into_raw(box_new_stu);
unsafe {
fill_data(p_stu);
print_data(p_stu);
println!("rust side print Bob: {:?}", Box::from_raw(p_stu));
}
}
将 C 端生成的 libcfoo.so
放到工程的根目录,使用
RUSTFLAGS='-L .' cargo build
编译。
然后在工程的根目录,使用下面指令运行:
LD_LIBRARY_PATH="." target/debug/example_09
会得到如下输出:
rust side print new_stu: CStudent { num: 0, total: 0, name: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [0.0, 0.0, 0.0] }
C side print: 2 Bob 100 60.60 70.70 80.80
rust side print Bob: CStudent { num: 2, total: 100, name: [66, 111, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [60.6, 70.7, 80.8] }
可以看到,达到了我们的预期目标:在 Rust 中分配内存创建的结构体,在 C 中填充其内容并返回。
所有权分析与智能指针 Box
整个 Rust 代码,首先实现Default
初始化结构体并打印;其次调用了导出的 C 函数 fill_data
,并在 C 端打印填充结构体的数据;最后再次打印。
在 Rust 中初始化的结构体,要将其传递到 C 函数中进行数据填充时,我们使用了 Rust 的智能指针 Box
。我们知道 Rust 与 C/C++ 不同的是,它不需要开发者显式地调用函数去分配和回收内存。而智能指针 Box
由于实现了 Drop
从而提供自动释放堆内存的功能,我们使用到它提供的两个方法:
fn into_raw(b: Box<T>) -> *mut T
unsafe fn from_raw(raw: *mut T) -> Box<T>
所有权分析如下:
(1)首先使用Box
分配一块堆内存,并使用 Box::into_raw
函数(标准库描述:https://doc.rust-lang.org/beta/std/boxed/struct.Box.html#method.into_raw)返回其原始指针,在确保和 C 端内存对齐的同时,完成所有权的转移,也就是说执行后, p_stu
负责了由之前 box_new_stu
管理的内存。
(2)然后调用 C 端的函数 fill_data
填充数据,并调 C 端函数 print_data
打印填充后的数据。
(3)最后在 Rust 端再次打印填充后的数据,其中使用了 Box::from_raw
函数(标准库描述:https://doc.rust-lang.org/beta/std/boxed/struct.Box.html#method.from_raw)将原始指针转换回 Box
,所有权又转移到 Rust 这边,从而由 Rust 的 RAII 规则允许 Box
析构函数执行清除操作,正确销毁并释放内存。这个方法必须用 unsafe 括起来调用。
Valgrind
Valgrind(https://valgrind.org/)是用于构建动态分析工具的基础框架。基于它的内存泄露检测工具 Memcheck (https://valgrind.org/info/tools.html#memcheck)可以自动检测许多内存管理和线程错误。
我们使用它验证程序的结果如下:
➜ example_09 git:(master) /usr/bin/valgrind --tool=memcheck --leak-check=full ./target/debug/example_09
==25534== Memcheck, a memory error detector
==25534== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25534== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==25534== Command: ./target/debug/example_09
==25534==
rust side print new_stu: CStudent { num: 0, total: 0, name: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [0.0, 0.0, 0.0] }
C side print: 2 Bob 100 60.60 70.70 80.80
rust side print Bob: CStudent { num: 2, total: 100, name: [66, 111, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [60.6, 70.7, 80.8] }
==25534==
==25534== HEAP SUMMARY:
==25534== in use at exit: 0 bytes in 0 blocks
==25534== total heap usage: 21 allocs, 21 frees, 4,473 bytes allocated
==25534==
==25534== All heap blocks were freed -- no leaks are possible
==25534==
==25534== For counts of detected and suppressed errors, rerun with: -v
==25534== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
其中25534
是进程ID,13 行显示:total heap usage: 21 allocs, 21 frees, 4,473 bytes allocated
,表明堆内存的使用情况,共发生 21 次分配和释放,内存大小为 4473 字节;同时 15 行显示:All heap blocks were freed -- no leaks are possible
, 它表明所有的堆内存已被释放,没有泄露。
如果删除掉这行代码 println!("rust side print Bob: {:?}", Box::from_raw(p_stu));
,大家觉得会有怎样的结果呢?
编译执行,程序运行正常,得到如下输出:
rust side print new_stu: CStudent { num: 0, total: 0, name: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [0.0, 0.0, 0.0] }
C side print: 2 Bob 100 60.60 70.70 80.80
但想一想,这样有问题吗?
我们再次执行工具 Memcheck 检测下是否有内存泄露?
==13973== Memcheck, a memory error detector
==13973== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==13973== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==13973== Command: ./target/debug/example_09
==13973==
rust side print new_stu: CStudent { num: 0, total: 0, name: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [0.0, 0.0, 0.0] }
C side print: 2 Bob 100 60.60 70.70 80.80
==13973==
==13973== HEAP SUMMARY:
==13973== in use at exit: 40 bytes in 1 blocks
==13973== total heap usage: 21 allocs, 20 frees, 4,473 bytes allocated
==13973==
==13973== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==13973== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==13973== by 0x10D22B: alloc::alloc::alloc (alloc.rs:81)
==13973== by 0x10D27B: <alloc::alloc::Global as core::alloc::AllocRef>::alloc (alloc.rs:172)
==13973== by 0x10D190: alloc::alloc::exchange_malloc (alloc.rs:225)
==13973== by 0x10E01B: new<example_09::CStudent> (boxed.rs:175)
==13973== by 0x10E01B: example_09::main (main.rs:63)
==13973== by 0x10DCFA: std::rt::lang_start::{{closure}} (rt.rs:67)
==13973== by 0x116DB2: {{closure}} (rt.rs:52)
==13973== by 0x116DB2: std::panicking::try::do_call (panicking.rs:303)
==13973== by 0x1185A6: __rust_maybe_catch_panic (lib.rs:86)
==13973== by 0x1177BB: try<i32,closure-0> (panicking.rs:281)
==13973== by 0x1177BB: catch_unwind<closure-0,i32> (panic.rs:394)
==13973== by 0x1177BB: std::rt::lang_start_internal (rt.rs:51)
==13973== by 0x10DCD6: std::rt::lang_start (rt.rs:67)
==13973== by 0x10E289: main (in /data/github/lester/rust-practice/ffi/example_09/target/debug/example_09)
==13973==
==13973== LEAK SUMMARY:
==13973== definitely lost: 40 bytes in 1 blocks
==13973== indirectly lost: 0 bytes in 0 blocks
==13973== possibly lost: 0 bytes in 0 blocks
==13973== still reachable: 0 bytes in 0 blocks
==13973== suppressed: 0 bytes in 0 blocks
==13973==
==13973== For counts of detected and suppressed errors, rerun with: -v
==13973== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
根据 Memcheck 的手册 (https://valgrind.org/docs/manual/mc-manual.html)分析结果:堆分配信息在 10,11 行显示的 in use at exit: 40 bytes in 1 blocks
和total heap usage: 21 allocs, 20 frees, 4,473 bytes allocated
表明有一块内存未释放;同时泄露信息在 31 行 definitely lost: 40 bytes in 1 blocks
,这意味着找不到指向该块的指针,可能未在程序退出时将其释放, 此类情况应由程序员解决。
结语
在 Rust 调用 C 时,使用 Box::into_raw
函数返回原始指针并转移所有权将该指针传给 C ,之后在 Rust 端必须显式的使用 Box::from_raw
函数将原始指针转换回 Box
,把所有权转移回来。这样才能正确销毁并释放内存。
完整的示例代码:https://github.com/lesterli/rust-practice/tree/master/ffi/example_09
评论区
写评论还没有评论