Rust FFI 与C语言互相调用

Rust FFI 与C语言互相调用

  • 参考
  • cbindgen 简介
    • 二进制方式构建
    • 脚本构建
  • Demo程序说明
    • 示例工程:
    • makefile
    • test脚本
  • 基本数据类型
    • Rust侧
    • C侧
  • 对象
    • Rust侧
    • C侧
  • slice
    • Rust侧
    • C侧
  • 字符串C传入
    • 不复制数据
      • Rust侧
      • C侧
    • 复制数据
      • Rust侧
      • C侧
  • 字符串Rust传出
    • C侧需要释放的方式
      • 1:提供专门的函数
        • Rust侧
        • C侧
      • 2:malloc函数传入rust
        • rust侧
        • C侧
      • 3:Rust调用glibc
        • Rust侧
        • C侧
    • 回调的方式
      • Rust侧
      • C侧
    • C传入缓冲区方式
      • Rust侧
      • C侧
  • 元组
    • Rust侧
    • C侧
  • vector给C
    • Rust侧
    • C侧
  • CMakeList.txt配置Rust编译
    • 父CMakeList.txt
    • rustlib目录下CMakeList.txt
    • 使用的模块

参考

https://cloud.tencent.com/developer/article/2077534
https://github.com/shepmaster/rust-ffi-omnibus

cbindgen 简介

二进制方式构建

$ cargo install cbindgen
//默认构建C++头文件 C语言需要 --lang C
$ cd /path/to/my/project && cbindgen . -o target/my_project.h

使用配置构建参考:https://github.com/eqrion/cbindgen/blob/master/docs.md

脚本构建

Cargo.toml

[package]
...
build="build.rs"

[build-dependencies]
...
cbindgen = "x.x.x"

build.rs 与 Cargo.toml同级目录,自用的build.rs配置

extern crate cbindgen;

use cbindgen::RenameRule::CamelCase;
use cbindgen::{StructConfig, ParseConfig, SortKey::Name};

use std::env;
use std::path::PathBuf;
use cbindgen::Config;
use std::vec;


fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    let package_name = env::var("CARGO_PKG_NAME").unwrap();
    let output_file = target_dir()
        .join(format!("{}.hpp", package_name))
        .display()
        .to_string();

    let structure = StructConfig{
        rename_fields : CamelCase,
        ..Default::default()
    };

    let parse = ParseConfig {
        parse_deps: true,
        include: Some(vec![String::from("reqwest::blocking")]),
        ..Default::default()
    };
    let config = Config {
        namespace: Some(String::from("ffi")),
        includes: vec![String::from("ffi.hpp")],
        pragma_once: true,
        cpp_compat:true,
        sort_by: Name,
        structure,
        parse,
        ..Default::default()
    };

    cbindgen::generate_with_config(&crate_dir, config)
        .unwrap()
        .write_to_file(&output_file);
}

/// Find the location of the `target/` directory. Note that this may be
/// overridden by `cmake`, so we also need to check the `CARGO_TARGET_DIR`
/// variable.
fn target_dir() -> PathBuf {
    if let Ok(target) = env::var("CARGO_TARGET_DIR") {
        PathBuf::from(target)
    } else {
        PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("target")
    }
}

Demo程序说明

示例工程:

https://github.com/markrenChina/rust_ffi

Cargo.toml 可能需要

[dependencies]
libc = "0.2.139"

[lib]
crate_type = ["cdylib"]
name = "demo"
cargo build --release
make
./test_c

makefile

all:demo

demo:main.c
	#或者在这加入cargo build --release
    gcc -o demo main.c  -I/usr/include -L./target/release -ldemo

clean:
    rm demo

test脚本

chmod +x test_c

export LD_LIBRARY_PATH=$PWD/target/release:$LD_LIBRARY_PATH

./demo

基本数据类型

Rust侧

#[no_mangle]
pub extern "C" fn addition(a: u32, b: u32) -> u32 {
    a + b
}

C侧

#include 
#include 
#include 

extern uint32_t
addition(uint32_t, uint32_t);

int main(void) {
  uint32_t sum = addition(1, 2);
  printf("%" PRIu32 "\n", sum);
}

对象

这里的对象传递是通过在堆上建立Rust风格的struct内存区,传递给C的是对象的指针,C中保存指针,使用时传入Rust,Rust根据struct信息操作内存。
后面元组部分展示了#[repr©]的方式,直接建立C风格的struct内存区,C中可以直接操作struct内的内存。

Rust侧

Box::new将对象分配到堆上,
Box::into_raw(),获取原始指针,并且封印了这块堆上内存的析构,只有在Box::from_raw()之后,堆上这块内存的析构函数会重新开启,同时获得所有权。
ffi交互时,不同语言的栈内存结构(读写内存协议)存在差距,存在频繁使用堆内存的性能问题。

extern crate libc;

use libc::c_char;
use std::collections::HashMap;
use std::ffi::CStr;

pub struct ZipCodeDatabase {
    population: HashMap<String, u32>,
}

impl ZipCodeDatabase {
    fn new() -> ZipCodeDatabase {
        ZipCodeDatabase {
            population: HashMap::new(),
        }
    }

    fn populate(&mut self) {
        for i in 0..100_000 {
            let zip = format!("{:05}", i);
            self.population.insert(zip, i);
        }
    }

    fn population_of(&self, zip: &str) -> u32 {
        self.population.get(zip).cloned().unwrap_or(0)
    }
}

#[no_mangle]
pub extern "C" fn zip_code_database_new() -> *mut ZipCodeDatabase {
    Box::into_raw(Box::new(ZipCodeDatabase::new()))
}

#[no_mangle]
pub extern "C" fn zip_code_database_free(ptr: *mut ZipCodeDatabase) {
    if ptr.is_null() {
        return;
    }
    unsafe {
        Box::from_raw(ptr);
    }
}

#[no_mangle]
pub extern "C" fn zip_code_database_populate(ptr: *mut ZipCodeDatabase) {
    let database = unsafe {
        assert!(!ptr.is_null());
        &mut *ptr
    };
    database.populate();
}

#[no_mangle]
pub extern "C" fn zip_code_database_population_of(
    ptr: *const ZipCodeDatabase,
    zip: *const c_char,
) -> u32 {
    let database = unsafe {
        assert!(!ptr.is_null());
        &*ptr
    };
    let zip = unsafe {
        assert!(!zip.is_null());
        CStr::from_ptr(zip)
    };
    let zip_str = zip.to_str().unwrap();
    database.population_of(zip_str)
}

C侧

c++ 最好使用包装类,在析构中free对象

#include 
#include 
#include 

typedef struct zip_code_database zip_code_database_t;

extern zip_code_database_t *
zip_code_database_new(void);

extern void
zip_code_database_free(zip_code_database_t *);

extern void
zip_code_database_populate(zip_code_database_t *);

extern uint32_t
zip_code_database_population_of(const zip_code_database_t *, const char *zip);

int main(void) {
  zip_code_database_t *database = zip_code_database_new();
  zip_code_database_populate(database);

  uint32_t pop1 = zip_code_database_population_of(database, "90210");
  uint32_t pop2 = zip_code_database_population_of(database, "20500");

  zip_code_database_free(database);

  printf("%" PRId32 "\n", (int32_t)pop1 - (int32_t)pop2);
}

slice

Rust侧

extern crate libc;

use libc::size_t;
use std::slice;

#[no_mangle]
pub extern "C" fn sum_of_even(n: *const u32, len: size_t) -> u32 {
    let numbers = unsafe {
        assert!(!n.is_null());

        slice::from_raw_parts(n, len as usize)
    };

    numbers
        .iter()
        .filter(|&v| v % 2 == 0)
        .sum()
}

C侧

#include 
#include 
#include 

extern uint32_t
sum_of_even(const uint32_t *numbers, size_t length);

int main(void) {
  uint32_t numbers[] = {1, 2, 3, 4, 5, 6};
  size_t length = sizeof numbers / sizeof *numbers;
  uint32_t sum = sum_of_even(numbers, length);
  printf("%" PRIu32 "\n", sum);
}

字符串C传入

不复制数据

Rust侧

入参 *const c_char

extern crate libc;

use libc::c_char;
use std::ffi::CStr;

#[no_mangle]
pub extern "C" fn print_c_string(s: *const c_char) {
    let c_str = unsafe {
        assert!(!s.is_null());

        CStr::from_ptr(s)
    };

    let str = c_str.to_str().unwrap();
    println!("printed from rust: {:#?}",str);
}

C侧

#include 
#include 
#include 

extern uint32_t
print_c_string(const char *str);

int main(void) {
  print_c_string("göes to élevên");
}

复制数据

在to_owned时复制了数据,不怕C侧free掉原来的内存

Rust侧

extern crate libc;

use libc::c_char;
use std::ffi::CStr;

#[no_mangle]
pub extern "C" fn print_c_string(s: *const c_char) {
    let c_str = unsafe {
        assert!(!s.is_null());

        CStr::from_ptr(s)
    };

    let r_str = c_str.to_str().unwrap().to_owned();
    println!("printed from rust: {:#?}",r_str);
}

C侧

与前面一样

字符串Rust传出

C侧需要释放的方式

不推荐这几种方式,很容易忘记释放导致内存泄漏。

1:提供专门的函数

普通方式在C侧要主动销毁,推荐采用RAII方式。
这种方式是最普遍的,比如你调用某个rust函数,这个函数返回就是一个字符串,除非在包装上采用后面windows式的,不然使用这种方式返回字符串是最常见的。

Rust侧

示例中返回mut,推荐使用const,使用*const在free时,CString::from_raw()需要 as *mut _

extern crate libc;

use libc::c_char;
use std::ffi::CString;
use std::iter;

#[no_mangle]
pub extern "C" fn theme_song_generate(length: u8) -> *mut c_char {
    let mut song = String::from(" ");
    song.extend(iter::repeat("na ").take(length as usize));
    song.push_str("Batman! ");

    let c_str_song = CString::new(song).unwrap();
    c_str_song.into_raw()
}

#[no_mangle]
pub extern "C" fn theme_song_free(s: *mut c_char) {
    unsafe {
        if s.is_null() {
            return;
        }
        drop(CString::from_raw(s));
    };
}

C侧

#include 
#include 

extern char *
theme_song_generate(uint8_t length);

extern void
theme_song_free(char *);

int main(void) {
  char *song = theme_song_generate(5);
  printf("%s\n", song);
  theme_song_free(song);
}

2:malloc函数传入rust

rust侧

use libc::{c_char, c_void};
use std::ffi::CString;
use std::usize;


const STRING : &str= "hello markrenChina";

type Allocator = unsafe extern fn(usize) -> *mut c_void;

#[no_mangle]
pub unsafe extern fn get_string_with_allocator(allocator: Allocator) -> *mut c_char {
    let ptr: *mut c_char = allocator(STRING.as_bytes().len()).cast();
    copy_string(ptr);
    ptr
}

///这里展示与缓冲区方式不一样的函数copy api
#[no_mangle]
pub unsafe extern fn copy_string(ptr: *mut c_char) {
    let bytes = STRING.as_bytes();
    let len = bytes.len();
    std::ptr::copy(STRING.as_bytes().as_ptr().cast(), ptr, len);
    std::ptr::write(ptr.offset(len as isize) as *mut u8, 0u8);
}

#[no_mangle]
pub unsafe extern fn free_string(ptr: *const c_char) {
    let _ = CString::from_raw(ptr as *mut _);
}

C侧

#include 
#include 
#include 
#include 
#include 

typedef void *(*Allocator)(uintptr_t);

char *get_string_with_allocator(Allocator allocator);

/**
 *这里展示与缓冲区方式不一样的函数copy api
 */
void copy_string(char *ptr);

void free_string(const char *ptr);

int main() {
  char* rust_string = get_string_with_allocator(malloc);
  printf("%s\n",rust_string);
  free(rust_string);  //This use free not free_string
}

3:Rust调用glibc

具体见示例工程

Rust侧

#[no_mangle]
pub unsafe extern fn get_string_with_malloc() -> *mut c_char{
    let ptr: *mut c_char = libc::malloc(get_string_len()).cast();
    copy_string(ptr);
    ptr
}

C侧

int main() {
  char* rust_string = get_string_with_malloc();
  printf("%s\n",rust_string);
  free(rust_string);  //This use free not free_string
}

回调的方式

回调方式不需要C侧主动释放,因为是借用Rust的字符串,但是可能需要注意线程。

Rust侧

use std::ffi::*;

type Callback = unsafe extern fn(*const c_char);

#[no_mangle]
pub unsafe extern "C" fn rust_call_c(callback: Callback ){
    let c_string = CString::new("su").expect("CString new failed");
    callback(c_string.as_ptr())
}

C侧

#include 

typedef void (*Callback)(const char*);

void rust_call_c(Callback callback);


void callback(const char* string) {
    printf("printed from C: %s \n", string);
}

int main() {
    rust_call_c(callback);
    return 0;
}

C传入缓冲区方式

windows系统调用常见的方式。
不同上面的C侧主动释放的方式,传入缓冲区的方式是,C侧申请(malloc)C侧释放(free)的方式是常规的代码操作。
c++可以通过包装,传出std::string,析构释放。

Rust侧

use libc::{c_char, c_int };
use std::{slice, ptr ,usize};

const HELLO: &str = "hello worls";

#[no_mangle]
pub unsafe extern "C" fn get_string_api(buffer: *mut c_char, length: *mut usize) -> c_int {
    if buffer.is_null() {
        if length.is_null(){
            return -1;
        }else {
            *length = HELLO.asbytes().len() + 1;
            return 0;
        }
    }

    let buffer = slice::from_raw_parts_mut(buffer as *mut u8, *length);

    if HELLO.len() >= buffer.len() {
        return -1;
    }

    ptr::copy_nonoverlapping(
            HELLO.as_ptr(),
            buffer.as_mut_ptr(),
            HELLO.len(),
            );

    buffer[HELLO.len()] = 0;

    HELLO.len() as c_int
}

C侧

#include 
#include 
#include 
#include 
#include 

/**仿windows系统函数的方式    
 * if buffer NULL @return need len
 * @retrun -1 fail
 */
int get_string_api(char *buffer, uintptr_t *length);

int main(){
  size_t* len;
  get_string_api(NULL,len);
  printf("len = %ld\n", *len);

  char * buff = malloc((*len) * sizeof(char ));
  get_string_api(buff,len);
  printf("string = %s\n", buff);

  free(buff);
}

元组

#[repr©]这种以C语言方式的结构体内存布局方式,能被大部分高级语言解析并使用。

Rust侧

示例中#[repr©]建立一个名为Tuple 的结构体,命名上有些误解。在Rust侧,元组的写法为(a,b)。2个From实现,分别为rust的元组转Tuple结构体,Tuple结构体转rust元组。
如果有需求要传递一个#[repr©]对象数组,在支持initializer_list版本的C++中,可以在C++层包装一个initializer_list版本的函数,采用{{},{}}的方式入参。交互起来非常方便。

use std::convert::From;

// A Rust function that accepts a tuple
fn flip_things_around_rust(tup: (u32, u32)) -> (u32, u32) {
    let (a, b) = tup;
    (b + 1, a - 1)
}

// A struct that can be passed between C and Rust
#[repr(C)]
pub struct Tuple {
    x: u32,
    y: u32,
}

// Conversion functions
impl From<(u32, u32)> for Tuple {
    fn from(tup: (u32, u32)) -> Tuple {
        Tuple { x: tup.0, y: tup.1 }
    }
}

impl From<Tuple> for (u32, u32) {
    fn from(tup: Tuple) -> (u32, u32) {
        (tup.x, tup.y)
    }
}

// The exported C method
#[no_mangle]
pub extern "C" fn flip_things_around(tup: Tuple) -> Tuple {
    flip_things_around_rust(tup.into()).into()
}

C侧

#include 
#include 
#include 

typedef struct {
  uint32_t x;
  uint32_t y;
} tuple_t;

extern tuple_t
flip_things_around(tuple_t);

int main(void) {
  tuple_t initial = { .x = 10, .y = 20 };
  tuple_t result = flip_things_around(initial);
  printf("(%" PRIu32 ",%" PRIu32 ")\n", result.x, result.y);
}

vector给C

Rust侧

拿到c侧的二级指针,rust侧得到vec后将二级指针的一级指针内存改为vec的地址。再主动让内存遗忘这块区域。

extern crate libc;

use libc::size_t;
use std::mem;

#[no_mangle]
pub extern "C" fn counter_generate(size: size_t, vec: *mut *mut i16) -> size_t {
    let mut counted: Vec<_> = (0..).take(size).collect();

    counted.shrink_to_fit();
    let ret = counted.len();
    unsafe { *vec = counted.as_mut_ptr() };
    mem::forget(counted);

    ret
}

#[no_mangle]
pub extern "C" fn counter_free(arr: *mut i16, size: size_t) {
    unsafe {
        if arr.is_null() {
            return;
        }

        Vec::from_raw_parts(arr, size, size)
    };
}

C侧

先在C栈上分配一个指针,传给rust的是2级指针(副作用编程,回传后一级指针的内容修改为数组首地址)。

#include 
#include 
#include 

extern size_t
counter_generate(size_t size, int16_t **vec);

extern void
counter_free(int16_t *vec, size_t size);

int main(void) {
  int16_t *vec;
  size_t vec_len = counter_generate(10, &vec);
  for (size_t i = 0; i < vec_len; i++) {
    printf("%" PRId16 "..", vec[i]);
  }
  printf("\n");

  counter_free(vec, vec_len);
}

CMakeList.txt配置Rust编译

简单介绍一下,使用时,在根目录下新建build文件夹,编译输出在这个文件夹下

cd build && cmake.. && make

假设我们的rust 模块叫 rustlib

父CMakeList.txt

add_subdirectory(rustlib)

rustlib目录下CMakeList.txt

# cargo 设置
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(CARGO_CMD cargo build)
    set(TARGET_DIR "debug")
else ()
    set(CARGO_CMD cargo build --release)
    set(TARGET_DIR "release")
endif ()

set(RUSTLIB_SO "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_DIR}/librustlib.so")


add_custom_target(rustlib ALL
    COMMENT "Compiling rustlib module"
    COMMAND CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} ${CARGO_CMD}
    COMMAND cp ${RUSTLIB_SO} ${CMAKE_CURRENT_BINARY_DIR}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(rustlib PROPERTIES LOCATION ${CMAKE_CURRENT_BINARY_DIR})

add_test(NAME rustlib_test
    COMMAND cargo test
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})

使用的模块

配置CMakeList.txt

set(RUSTLIB_BUILD_DIR ${CMAKE_BINARY_DIR}/rustlib)
include_directories(${RUSTLIB_BUILD_DIR })
...
get_target_property(RUSTLIB_DIR rustlib LOCATION)
target_link_libraries(gui ${RUSTLIB_DIR }/librustlib.so)
add_dependencies(xxx rustlib)

你可能感兴趣的:(Rust,rust,c语言,开发语言)