Rust FFI 编程 - 手动绑定 C 库入门 04

2020-06-28 12:04:30 浏览数 (1)

本篇,我们说明 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 端代码如下:

代码语言:javascript复制
// 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 %.2fn",
           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 中如下:

代码语言:javascript复制
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 会得到如下输出:

代码语言:javascript复制
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)可以自动检测许多内存管理和线程错误。

我们使用它验证程序的结果如下:

代码语言:javascript复制
➜  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));,大家觉得会有怎样的结果呢?

编译执行,程序运行正常,得到如下输出:

代码语言:javascript复制
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 检测下是否有内存泄露?

代码语言:javascript复制
==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 blockstotal 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

0 人点赞