搜索
您的当前位置:首页正文

外部函数接口FFI

来源:易榕旅网

在某些场景下,你的RUST代码可能需要与另外一种语言编写的代码进行交互。RUST为此提供了extern关键字来简化创建和使用外部函数接口(Foreign Function Interface,FFI)。FFI是编程语言定义函数的一种方式,它允许其它编程语言来调用这些函数。

一个引子

下面例子中的abs并没有定义具体的实现,尽管通过字面量可以知道这是取绝对值的操作,程序正常输出的结果为Hello, world! 3,看样子abs是有具体实现的,并且rust也找到了具体的实现。我很怀疑函数abs是在哪里定义的实现呢?这种rust模式的函数声明怎么就正常执行了呢?

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Hello, world! {}", abs(-3));
    }
}

这段代码在extern "C"列出了我们想要调用的外部函数名称及签名,其中的"C"指明了外部函数使用的二进制接口(Application Binary Interface,ABI):它被用来定义函数在汇编层面的调用方式。我们使用的"C" ABI正是C编程语言的ABI,也是最常见的ABI格式之一。

ABI的解释下面会继续,代码使用unsafe括起来标识执行一段不安全的代码。这段示例代码中使用到了extern "C",这属于关键字定义,应该不可能出现 extern "B"吧!我比较在意的在于:除了abs这个函数,我应该怎么自定义一个函数放到extern的作用域内呢?

我是这么想的,我可以在本地编译多个动态库,这些库可以动态加载到RUST主程序中,而两者之间通讯的协议就是extern C。上面的代码好像也有这么点意思哈,把abs看做在lib中定义的方法就是这个意思。如果真如下面这种模式,动态插件加载基本八字有一撇了。

ABI前世今生

引用的文章来了:,大家跳转过去瞅瞅。在计算机软件中,应用二进制接口(ABI)是两个二进制程序模块之间的接口;通常这些模块之一是库或操作系统工具,而另一个是用户正在运行的程序。

C-ABI包含两个关键的内核:

  • 数据的内存布局方式
  • 函数如何调用

RUST目前的ABI并不稳定,即RUST不保证内存中数据结构的调用约定和内存布局不被改变。这里有几个示例来说明什么是不稳定的ABI。了解这里,我其实挺吃惊的,吃惊居然还有这种操作,而我居然还不知道,我不禁怀疑,我看到的到底是不是真的,还能不能相信自己的眼睛…

// 虽然下面的结构体本质是相同的,但是 Rust 编译器不保证给予它们字段相同的内存偏移量
struct A(u32, u64);
struct B(u32, u64);

// Rust 编译器不保证字段的顺序和定义的一样
struct Rect {
    x: f32,
    y: f32,
    w: f32,
    h: f32,
}

写代码的时候,我以为我为路边的下水道设计了“方形”的井盖,但RUST内部给我优化成了“圆形”的井盖。作为一个程序猿,我本不该操心系统内部是怎么实现的,就好比我完全不关心我写的代码最终是如何变成机器码一样。但现实是ABI它关心啊,它需要一个明确的交互协议,而不是变化的。

RUST编译器会对上面的结构体进行优化,如果内存布局是确定的,就不利于优化了。比如没有办法对结构体字段进行重排以便达到最小化内存占用的优化目标。内存布局不确定性也有利于模糊测试(Fuzzer),因为模糊测试需要将字段随机排列以便更容易地暴露潜在的问题。

基于内存不稳定的原因,我么引出一个关键的注释声明#[repr(C)]来试图解决这个问题,这个声明后面拎出来说单独解释一下。但这个声明并不是银弹,还是有解决不了的场景。

#[repr(C)]
struct MyStruct {
    x: u32,
    y: Vec<u8>,
    z: u32,
}

对于该示例来说,虽然使用了#[repr(C)]让结构体字段的顺序确定了,但是字段的偏移量依然无法确定,因为Vec<8>没有任何确定性的排序,从而z的偏移量是无法确定的。所以这种类型不适合使用CFFI

关于文章的解释:MyStructVec<8>没有任何确定性的顺序,我其实不理解。如果vec是胖指针,如果字段x\y\z顺序固定了,其实是有可能让内存结构固定的。我们设想下面两种内存布局形式,第一种可能就是固定的。不过还有个问题,就是堆区分配和栈区分配的问题。

作者尝试动态加载实现插件,发现RUST ABI不稳定带来的问题比想象的更加严重。在这之前,他一直认为即使RUST ABI不稳定,只要库和主二进制文件是用相同的编译器以及std等版本编译的,就可以安全地动态加载一个库。然而事实证明,ABI不仅仅是可能在不同编译版本之间发生”断裂“,在编译器执行的过程中也会发生断裂,即RUST编译器并不保证同一个类型的布局在每次执行的时候都一致,类型布局可以随着每次编译而改变。所以他的方案是使用#[repr(C)]C-ABI以及使用abi_stable来获得稳定的std库。

作者后面尝试使用abi_stable来开发插件系统。abi_stable是按模块来构建的,并且提供了很多FFI安全的类型(指FFI边界提供了稳定的内存布局),包括trait对象的支持及提供了处理FFI边界panic的方法。

repr(C)定义兼容C的内存布局

要定义兼容C结构体的RUST结构体类型,需要使用repr(C)属性。在结构体声明上注释#[repr(C)],表示让RUST在内存中使用与C布局其结构体相同的方式来布局当前的结构体。

use std::ffi::{c_char, c_int};

#[repr(C)]
pub struct git_error {
    pub message: *const c_char,
    pub klass: c_int,
}

#[repr(C)]属性只影响结构体本身的布局。例子中的结构体要匹配C结构体,结构体的字段类型就必须使用C的类型:*const c_char对应char *等。这里面的std::ffi值的我们去深究一下,RUST里也有字符和整数类型,现在专门引入c_char\c_int本身就很唐突,这种情况是必须的呢,还是可选的呢?我们其实也不知道。

std::ffi的概述介绍重点其实是字符串类型的介绍,RUSTC的字符串存储上本身存在差异,肯定需要一种转换手段。RUST程序里调用C的实现,C里也会调用RUST的实现吗?CStingCStr就是我们的突破口,而且上面对extern中声明的方法也没有讲述完整,有必要画上一个句号。我们单独拎出一个小结来补充,现在让内容重新回归到repr(C)

这么看来,RUSTC之间传递字符串就很困难。C将字符串表示为一个指向字符数组的指针,以一个空字符结束。而RUST可能保存为String,也可能是一个胖指针&str。这意味着不能把RUST字符串借用为C字符串。

CString和CStr使用

基于对RUST之前的概念了解,可以从对值的所有权、变量可读或者变量可写的角度来区分,如何区分CStringCStr这两个概念呢?CString的解释是From Rust to C,represents an owned, C-friendly string,可以理解为在RUST中使用CString生成一个传递给C的字符串。

CStr的解释是From C to Rust,represents a borrowed C string,可以理解为在C语言中生成的字符串,现在要在RUST中使用的话要通过CStr来转换。这里的borrowed也得特别注意,它表示不包含字符串的所有权,只能读不能写。RUSTC之间的这种内存转换传递其实挺有风险的,就是要做到读和写的绝对分离,同一个字符串如果RUSTC都在更改就无法控制。

更好的理解方式还是要show code,最好是展示一个完整的有因有果的例子,前面的例子都有些丈二和尚摸不着头脑。我们需要一个有意义的例子,要有解决现实问题的价值,但很遗憾的是现在没有。下面代码本意就是:RUST调用C语言中定义的方法strlen。每个RUST都可以链接到标准C库,所以下面的extern "C"做的是C库函数的声明。

use std::ffi::CString;
use std::os::raw::c_char;

extern "C" {
    fn strlen(s: *const c_char) -> usize;
}

fn main() {
    let rust_str: &str = "sust string";
    let null_terminated: CString = CString::new(rust_str).unwrap();

    unsafe { assert_eq!(strlen(null_terminated.as_ptr()), 11) }
}

现在的例子和文章开头的abs例子没什么大的区别,核心区别在于处理的类型是字符串。首先还是得了解下标准C库下strlen的声明,很容易查询到 size_t strlen(const char *str)*const*mut会是我们后续经常接触的类型,它们被定义为RUST中的裸指针,它们之间的区别在于可读和可写。

我们结合下图看类型的对应关系,代码中引入了std::os::raw::c_char类型,此处了charstd::ffi::c_char有什么区别吗?如果两者没什么本质区别,就应该让代码看起来更加统一。不过,这个例子也就这样了,RUST中向C中传递字符串变量需要使用中间类型CString,至于这个类型的方法操作细节,这个就忽略了。

加载三方库

上面的示例中extern中的函数都是标准C库中的,既然可以默认链接到标准C库,是不是也可变链接到第三方库C库呢,当然可以。好家伙,看起来已经有点接近“插件动态加载”的模式了。我翻了一部分书和博客,发现了一些比较不错的示例。

首推的便是 内容,通过手动编写一个C函数,然后打包成一个静态库,再在RUST中调用它。我主要是想要了解关于动态加载的能力,而这其实就是这种能力的体现。尽管只是一个简单的示例,但它帮助我们理解后续更加复杂的内容。

下面的代码非常好理解,而multiply正是我们要通过C语言实现的部分。前面示例中extern都是标准C库的函数,现在我们要声明一个自定义的函数。RUST如何加载这个方法,去哪里加载这个方法又是一个问题。我们已定义个multiply.c的文件,文件的内容见第二段代码:

use std::ffi::c_int;

extern "C" {
    fn multiply(a: c_int, b: c_int) -> c_int;
}

fn main() {
    println!("FFI C extern");

    unsafe {
        println!("Calling function in C");

        let result = multiply(5000, 5);

        println!("Result: {}", result)
    }
}

下面便是multiply.c的内容:使用C语言实现了一个乘法操作。我对C语言也不了解,关于int32_t声明的后缀_t感觉非常的奇怪。别的文章介绍说意图是见文知意,t表示type,表示声明的是一种类型,好像也说的过去。

现在我们只需要把这个文件编译成一个“动态库”,让RUST加载到它的程序里就可以了。为什么动态库要加引号呢?我此时此刻还搞不清楚具体是什么类型的库。据我了解有很多类型的库,静态库、动态库等等。但其实也不影响我们具体操作,先当做动态库来表述就可以了。后面搞清楚了,再来补充一下。

#include <stdio.h>
#include <stdint.h>

int32_t multiply(int32_t a, int32_t b)
{
    printf("[C] Hello from C!\n");
    printf("[C] Input a is: %i \n", a);
    printf("[C] Input b is: %i \n", b);
    printf("[C] Multiplying and returning result to Rust..\n");

    return a * b;
}

接下来又是一个问题域:构建脚本。下面的代码属于构建脚本的内容,文件的名字为build.rs。经过下面的构建脚本一折腾,我们的程序可以正常执行。当然,构建脚本需要在Cargo.toml中进行声明指定

extern crate cc;

fn main() {
    cc::Build::new().file("src/multiply.c").compile("multiply");
}

下面便是Cargo.toml中的内容:

[package]
name = "rust-ffi-to-c"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

[build-dependencies]
cc = "1.0"

abi_stable 库的使用用例

针对RUST-to-RUST的动态调用,关注点在程序启动时的动态加载以及类型检查。这个库允许定义RUST在运行时动态加载的库,及时这些加载的库和主项目有不同版本的版本依赖。

这个库的使用场景:

  • RUST依赖树从静态编译转换为一个静态链接库、或动态链接库,从而允许对更改进行单独的重新编译。
  • 创建一个插件系统(不支持卸载插件)

插件系统是我关注的核心,但abi_stable在插件系统中究竟起了什么作用,为什么说abi_stable是稳定的内存结构体布局,我们该如何使用abi_stable呢?做为一个刚入门的人,abi_stable真的很难让人下手。

我们先了解点基础知识,在后续代码中会使用到的这些细节。static这个关键字在Go语言中应该是不存在的,全局变量和static变量有什么区别呢?

常量

const关键字定义常量,可以标记公开属性,而且必须声明类型。常量名全部使用大写字母也是常量的惯用命名方式。

pub const DISTANCE: f64 = 20.0

static关键字定义静态变量,跟常量差不多:

pub static DISTANCE: f64 = 20.0

常量有点类似C++#define,即它的值会编译到代码中使用它的每个地方。常量不可修改,但静态变量可以被修改。不过,RUST没办法保证对可修改静态变量的专有访问权,可修改的静态变量本质上是不安全的。

所以说,静态变量最好定义为不可修改的类型。

下面开始看

#[sabi(kind(Prefix( .. )))]

因篇幅问题不能全部显示,请点此查看更多更全内容

Top