C/C++调用Rust编写的动态库

C/C++调用Rust编写的动态库

一、背景

Rust通过大量的编译期检查能够有效避免程序运行时出现的各种内存问题,而且性能又仅次于C/C++,期望用Rust编写动态库供C/C++的项目使用。(本人初学 Rust,下文中不当之处还望指正)

二、解决方案

C/C++侧需要使用的函数为

Ret (*filter) (unsigned char* ,size_t);

其中 Ret 是从 Rust 侧的返回值,期望能够直接在 C/C++ 测作为 unsigned char* 使用

mod bind;
use once_cell::sync::Lazy;
static mut INSTANCE:Lazy<Box<dyn bind::Cfunc>>=Lazy::new(||{
    Box::new(Test{})
});
pub struct Test {
    
}
impl bind::Cfunc for Test {
    fn do_filter(&self,content:Vec<u8>)->Vec<u8> {
        println!("{:?}",content);
        content
    }
}
use core::slice;
use std::{os::raw::{c_uchar,c_uint}};
extern crate libc;
#[repr(C)]
struct Ret {
    ptr: *const c_uchar,
    len: c_uint,
    cap: c_uint
}
#[no_mangle]
extern "C" fn filter(content:*const c_uchar,len:c_uint)->Ret {
    unsafe {
        let bytes=slice::from_raw_parts(content, len as usize);
        let mut result=crate::INSTANCE.do_filter(bytes.to_vec());
        let ptr_=result.as_mut_ptr();
        let len_=result.len();
        let cap_=result.capacity();
        std::mem::forget(result);
        Ret{ptr:ptr_,len:len_ as c_uint,cap:cap_ as c_uint}
    }
    
}
#[no_mangle]
extern "C" fn rust_module_free(obj:Ret) {
    unsafe {
        Vec::<u8>::from_raw_parts(obj.ptr as *mut u8, obj.len as usize, obj.cap as usize);
    }
}
pub trait Cfunc {
    fn do_filter(&self,content:Vec<u8>)->Vec<u8>;
}

Rust 侧的实现如上述代码,最后的效果是 C/C++ 可以调用 Test::do_filter。其中值得注意的有下面几点

  1. Rust 内存的申请和释放本来有编译器管理,使用 std::mem::forget 可以让编译器遗忘这块内存,从而保证在函数结束时内存不会被释放
  2. Rust 申请的内存一定要让 Rust 来释放,原因是 Rust 堆内存的管理和 C/C++ 有所不同,所以对于此类的API,一定要提供一个类似于上述 rust_module_free 的函数,在 C/C++ 使用完 Rust 的 API 后,让 Rust 释放之前申请过的内存。
  3. Rust 释放 Vec 类型内存的方式是使用 Vec::from_raw_parts 重新加载内存中对应的 Vec 结构,从该函数的定义可知,Rust 的 Vec 主要需要维护三个结构,一处连续内存块的指针 ptr,数组实际长度 len,数组容量 capacity,这解释了 Ret 结构为何要如此定义。
  4. slice::from_raw_parts 与 Vec::from_raw_parts 的区别是,slice::from_raw_parts 不会释放 ptr 所指位置的内存。

三、测试

3.1 正确性检验

编写如下代码即调用 Rust 生成的动态库:

#include <stdio.h>
#include <dlfcn.h>
#include <iostream>
struct Ret {
	unsigned char* ptr;
	unsigned int len;
	unsigned int cap;
};
Ret (*filter) (unsigned char* ,size_t);
void (*rust_module_free) (Ret);
int main() {
	unsigned char buf[]={1,2,3,4,5,9,8,7,6};
	auto module=dlopen("./target/debug/libmyapp.so",RTLD_LAZY);
	filter=(decltype(filter))dlsym(module,"filter");
	rust_module_free=(decltype(rust_module_free))dlsym(module,"rust_module_free");
	auto ret=filter(buf,9);
	printf("ptr:%p len:%d cap:%dn",ret.ptr,ret.len,ret.cap);
	for (int i=0;i<ret.len;i++) 
	printf("%02x ",ret.ptr[i]);
	rust_module_free(ret);
	printf("nfreedn");
} 

如下图所示,程序的成功调用了 Rust 的 API
在这里插入图片描述

3.2 内存安全检验

Rust 相较于 C/C++ 最大的优点莫过于内存安全的保证,假如在调用Rust编写的动态库时出现了内存问题,可谓是适得其反,下面介绍一下如何对调用过程的内存安全进行检验。
首先对上文中的程序用 asan 检验一下,使用如下命令编译运行:

g++ test_linux.cpp -g -ldl -fsanitize=address -fsanitize-recover=address
./a.out

运行发现程序总体上没有内存相关的异常,说明 buf 没有被 Rust 的 API 意外释放。(虽然 buf 在栈上,释放这个词不太严谨,当然也可以把 buf 定义为 std::vector<unsigned char> 来直接验证堆上的情形)。因为假如说被释放,asan 会报内存异常的错误。可以在释放 ret 后添加如下代码来触发这个异常:

ret.ptr=buf;
rust_module_free(ret); 

在这里插入图片描述
然而,程序整体上用 asan 扫不出异常,并不代表程序内没有内存问题,还有一个潜在的问题是,rust 返回的 ret 是否被 rust_module_free 正确释放。由于程序结束时会将堆上的内存全部返还给操作系统,所以就算没有被正确释放,asan 也不会报错(注释掉 rust_module_free 那行代码重新编译运行,可以看出 asan 同样没有报错)。所以需要一个有效的方法检测 ret 是否被正确释放。
有两个方法可以进行间接的检验:

  1. 可以调用 rust_module_free 重复释放 ret,编译运行后可以发现 double free 的报错
    在这里插入图片描述
  2. 在 rust_module_free 后再次访问 ret 中 ptr 指针所指的内存,asan 会有踩内存的报错
    在这里插入图片描述
    以上的操作可以基本验证 ret 被 Rust 正确释放。