关于rust:Rust通过FFI调用C

8次阅读

共计 4524 个字符,预计需要花费 12 分钟才能阅读完成。

FFI存在背景

FFI(Foreign Function Interface)能够用来与其它语言进行交互,然而并不是所有语言都这么称说,例如 Java 称之为 JNI(Java Native Interface)

FFI 之所以存在是因为事实中很多代码库都是由不同语言编写的,如果咱们须要应用某个库,然而它是由其它语言编写的,那么往往只有两个抉择:

  • 对该库进行重写或者移植
  • 应用 FFI

FFI实现须要面对的挑战

  • 调用方语言是否涵盖了被调用语言的数据类型,rust作为调用方语言,涵盖了所有 C 语言的数据类型,比方 C 当中的 int,double 对应了 rust 中的 i32,f64 类型
  • 是否可能解决 C的裸指针,包含指向被看作是字符串的数组指针,包含构造体指针

Rust 中调用 C 的办法

  1. Rust 代码中应用 extern 关键字申明要调用的 C 函数
  2. 应用 unsafe 块调用它

留神点:须要手动解决参数和返回值的转换,可能产生异样报错

FFI实现调用简略 C 函数

试验平台Ubuntu22.04 amd64 Desktop

调用了 C 规范库当中的数学库函数,abs求绝对值,pow求幂,sqrt求平方根

use std::os::raw::{c_double, c_int};

// 从规范库 libc 中引入三个函数。// 此处是 Rust 对三个 C 函数的申明:extern "C" {fn abs(num: c_int) -> c_int;
    fn sqrt(num: c_double) -> c_double;
    fn pow(num: c_double, power: c_double) -> c_double;
}

fn main() {
    let x: i32 = -123;
    // 每次调用都必须产生在一个 unsafe 区域内, 表明 Rust 对外部调用中可能存在的不平安行为不负责
    println!("{x}的绝对值是: {}.", unsafe { abs(x) });
    let n: f64 = 9.0;
    let p: f64 = 3.0;
    println!("{n}的 {p} 次方是: {}.", unsafe { pow(n, p) });
    let mut y: f64 = 64.0;
    println!("{y}的平方根是: {}.", unsafe { sqrt(y) });
    y = -3.14;
    println!("{y}的平方根是: {}.", unsafe { sqrt(y) }); //** NaN = NotaNumber(不是数字)}

上述程序的输入是

-123 的绝对值是: 123.
9 的 3 次方是: 729.
64 的平方根是: 8.
-3.14 的平方根是: NaN.

FFI调用自定义地位的 C

我的项目构造如下

.
├── build.rs
├── Cargo.lock
├── Cargo.toml
├── hello
│   └── hello.c
└── src
    └── main.rs

编辑 hello/hello.c 文件

#include <stdio.h>

void hello() {printf("Hello, build script!!!!\n");
}

Cargo.toml 退出构建时依赖

[build-dependencies]
cc = "1.0"

批改build.rs

fn main() {
    // 示意在 hello/hello.c 文件产生批改的时候须要从新运行 build 脚本
    println!("cargo:rerun-if-changed=hello/hello.c");

    let mut builder: cc::Build = cc::Build::new();
    builder
        .file("./hello/hello.c")
        .compile("hello");
}

批改src/main.rs

// 应用 extern 申明 hello 是 C 外面的函数
extern "C" {fn hello();
}

fn main() {
    // rust 调用的时候须要应用 unsafe 包裹
    unsafe {hello();
    }
    println!("Hello, world!");
}

运行

$ cargo run
Hello, build script!!!!
Hello, world!

FFI调用简单函数

本示例蕴含构造体指针的传递

当初应用 C 库中的函数asctime

header文件地位是 /usr/include/time.h,函数定义如下,通过传入一个tm 类型的构造体指针,返回一个日期格局为 Day Mon dd hh:mm:ss yyyy\n 的字符串

/* Return a string of the form "Day Mon dd hh:mm:ss yyyy\n"
   that is the representation of TP in this format.  */
extern char *asctime (const struct tm *__tp) __THROW;

因为波及到大量的模板代码和类型转换,须要应用 bindgen 工具从 C 语言的头文件生成 rust 代码放慢开发速度,缩小低级谬误,提高效率

工具由 rust 语言官网保护,地址

https://github.com/rust-lang/rust-bindgen

Debian/Ubuntu系列的装置依赖

$ sudo apt install llvm-dev libclang-dev clang

其余零碎的装置依赖参考

https://rust-lang.github.io/rust-bindgen/requirements.html

装置命令行工具

$ cargo install bindgen-cli

比方当初转换 /usr/include/time.h 文件

rust 的我的项目根门路下执行命令

$ bindgen /usr/include/time.h > src/mytime.rs

之后比照 /usr/include/time.hsrc/mytime.rs

查看 C 语言的原始代码,找到函数 asctime 和构造体 tm 的定义

/* 源码地位 /usr/include/time.h  */
/* Return a string of the form "Day Mon dd hh:mm:ss yyyy\n"
   that is the representation of TP in this format.  */
extern char *asctime (const struct tm *__tp) __THROW;

/* 源码地位 /usr/include/x86_64-linux-gnu/bits/types/struct_tm.h
尽管执行的命令是 bindgen /usr/include/time.h > src/mytime.rs
然而工具依然会主动扫描转换相干头文件定义的构造体类型
*/
/* ISO C `broken-down time' structure.  */
struct tm
{int tm_sec;            /* Seconds.    [0-60] (1 leap second) */
  int tm_min;            /* Minutes.    [0-59] */
  int tm_hour;            /* Hours.    [0-23] */
  int tm_mday;            /* Day.        [1-31] */
  int tm_mon;            /* Month.    [0-11] */
  int tm_year;            /* Year    - 1900.  */
  int tm_wday;            /* Day of week.    [0-6] */
  int tm_yday;            /* Days in year.[0-365]    */
  int tm_isdst;            /* DST.        [-1/0/1]*/

# ifdef    __USE_MISC
  long int tm_gmtoff;        /* Seconds east of UTC.  */
  const char *tm_zone;        /* Timezone abbreviation.  */
# else
  long int __tm_gmtoff;        /* Seconds east of UTC.  */
  const char *__tm_zone;    /* Timezone abbreviation.  */
# endif
};

#endif

查看 src/mytime.rs 文件外面的代码,找到 asctime 函数以及 tm 构造体的定义

extern "C" {pub fn asctime(__tp: *const tm) -> *mut ::std::os::raw::c_char;
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct tm {
    pub tm_sec: ::std::os::raw::c_int,
    pub tm_min: ::std::os::raw::c_int,
    pub tm_hour: ::std::os::raw::c_int,
    pub tm_mday: ::std::os::raw::c_int,
    pub tm_mon: ::std::os::raw::c_int,
    pub tm_year: ::std::os::raw::c_int,
    pub tm_wday: ::std::os::raw::c_int,
    pub tm_yday: ::std::os::raw::c_int,
    pub tm_isdst: ::std::os::raw::c_int,
    pub tm_gmtoff: ::std::os::raw::c_long,
    pub tm_zone: *const ::std::os::raw::c_char,
}

批改 src/main.rs 文件内容

use std::ffi::{c_char, CStr, CString};

// mytime.rs 作为 main.rs 的一个 mod
mod mytime;

fn main() {
    // 因为 mytime::mm 构造体外面的 tm_zone 字段是一个字符串指针
    // 先创立一个字符串
    let timezone = CString::new("UTC").unwrap();

    // 从 mytime 中导入一个 tm 构造体,填写参数如下
    let mut time_value = mytime::tm {
        tm_sec: 1,
        tm_min: 1,
        tm_hour: 1,
        tm_mday: 1,
        tm_mon: 1,
        tm_year: 1,
        tm_wday: 1,
        tm_yday: 1,
        tm_isdst: 1,
        tm_gmtoff: 1,
        // 此处转换为指针类型
        tm_zone: timezone.as_ptr()};

    unsafe {
        // 裸指针
        let c_time_value_ptr = &mut time_value;
        // 获取一个 c 字符串指针
        let asctime_result_ptr = mytime::asctime(c_time_value_ptr);
        // 从指针再转变为 CStr 类型
        let c_str = CStr::from_ptr(asctime_result_ptr);
        // 最初再转变为 rust 的 &str 类型,返回的是一个 Result 类型,可能会产生 utf- 8 编码谬误
        println!("{:?}", c_str.to_str());
    }
}

运行

$ cargo run
Ok("Mon Feb  1 01:01:01 1901\n")

因为 bindgen 是把整个 /usr/include/time.h 文件外面的所有函数和构造体都转换到 rust 中,所以编译 rust 的时候会产生很多的 function *** is never used,constant **** should have an upper case name 这种告警,这个是失常的

参考浏览

Rust 调用 C 库函数 – linux cn

Rust调用 C 程序的实现步骤

C 规范库

rust语言圣经 – FFI

正文完
 0