Rust跨语言调用FFI外部函数接口

内容纲要

1. 背景知识

FFI(Foreign Function Interface)外部函数接口:用于规范语言间调用的语言特征。

FFI技术的主要功能是:

将一种编程语言的语义和调用约定与另一种编程语言的语义和调用约定相匹配。

所谓匹配是指:

不管哪种编程语言,无论是编译执行还是解释执行,最终都会到达处理器指令环节,在该环节,编程语言之间的语法、数据类型等语义差异均已消除,只需匹配调用约定即可,从而可以实现不同编程语言之间的相互调用。

匹配调用约定,需要用到应用程序二进制接口(ABI)。

何为ABI?ABI是一个规范,主要涵盖以下内容:

调用约定。一个函数的调用过程本质就是参数、函数、返回值如何传递。编译器按照调用规则去编译,把数据放到相应的堆栈中,函数的调用方和被调用方(函数本身)都需要遵循这个统一的约定。

内存布局。规定了大小和对齐方式。

处理器指令集。不同平台的处理器指令集不同。

目标文件和库的二进制格式。

ABI规范由编译器、操作平台、硬件厂商等共同指定。ABI是二进制层面程序兼容的契约,只有拥有相同的ABI,来自不同编译器之间的库才可以相互链接和调用,否则将无法链接,或者即使可以链接,也无法正确运行。

由于C语言伴随着操作系统一路发展而来,使得C语言ABI是唯一通用稳定的标准ABI。

2. Rust 与各语言相互调用示例

Rust与各种语言之间的相互调用,具体可参看https://github.com/alexcrichton/rust-ffi-examples 中的例子,内含:

  • c-to-rust

  • cpp-to-rust

  • crystal-to-rust

  • csharp-to-rust

  • dart-to-rust

  • go-to-rust-cgo-dynamic

  • go-to-rust-cgo-static

  • haskell-to-rust

  • julia-to-rust

  • luajit-to-rust

  • node-to-rust

  • perl-to-rust

  • php-to-rust

  • python-to-rust

  • ruby-to-rust

  • rust-to-c

  • rust-to-cmake

  • rust-to-cpp

  • rust-to-fortran

  • rust-to-go-dynamic

  • swift-to-rust

3. Rust crate封装C/C++语言接口

3.1 Rust 库配置

除可执行文件外,Rust语言支持四种库:

crate-type
Rust
其他语言
动态库
dylib
cdylib
静态库
rlib
staticlib

对crate-type的设置项可为:

  • bin:表示生成一个可执行文件。要求程序中必须包含一个main函数。

  • lib:表示生成一个Rust库。这里lib是对Rust库的统称,具体生成什么库,由编译器自行决定。一般情况下,默认会产生rlib静态库。

  • rlib:静态Rust库,由Rust编译器来使用。

  • dylib:动态Rust库,由Rust编译器来使用。该类型在Linux上会创建*.so文件,在MacOSX上会创建*.dylib文件,在Windows上会创建*.dll文件。

  • staticlib:生成静态系统库。Rust编译器永远不会链接该类型库,主要用于和C语言仅需链接,达成和其他语言交互的目的。静态系统库在Linux和MacOSX上会创建*.a文件,在Windows上会创建*.lib文件。

  • cdylib:生成动态系统库。同样用于生成C接口,和其他语言交互。该类型在Linux上会创建*.so文件,在MacOSX上会创建*.dylib文件,在Windows上会创建*.dll文件。

crate-type可指定多个。如包A依赖包B,而包B需要生成一个staticlib静态系统库,则可为包B同时指定staticlib和rlib两种包类型:

[lib]
      crate-type = ["rlib", "staticlib"]

3.2 Rust中FFI相关关键字

  • extern关键字和extern块:

        extern关键字:通过extern关键字声明的函数,可在Rust和C语言中自由使用。

        extern块:若在Rust中调用C代码,则可以使用extern块,将外部的C函数仅需逐个标记,以供Rust内部调用。

  • #[no_mangle]:

        表示关闭Rust的name mangling函数名称修改功能。若不加这个属性,Rust编译器就会修改函数名,这是现代编译器为了解决唯一名称解析一起的各种问题所引入的技术。若函数名被修改了,那么在C代码中就无法按原名称调用。

  • unsafe:

        将函数用unsafe关键字标记,是为了提醒函数调用者,注意该函数在使用时可能存在的违反“契约”的风险。

  • #[repr©]:

        表示:do what C does. The order, size, and alignment of fields is exactly what you would expect from C or C++. Any type you expect to pass through an FFI boundary should have repr(C), as C is the lingua-franca of the programming world.

3.3 cbindgen/rust-bindgen——由Rust库生成C/C++接口header

推荐使用cbindgen或rust-bindgen 工具来管理FFI boundaries。

cbindgen:

creates C/C++11 headers for Rust libraries which expose a public C API.

cbindgen安装:

cargo install –force cbindgen

构建相应的cbindgen.toml配置文件,可参考配置模板。

生成C++ header file 命令为:

cbindgen --config cbindgen.toml --crate my_rust_library --output my_header.h

生成C header file命令为:(增加–lang c)

cbindgen --config cbindgen.toml --crate my_rust_library --lang -c --output my_header.h

3.4 build.rs文件

build.rs 文件内为Build Scripts,位于the root of a package,Cargo将compile the script and execute it just before building the package。

若调用cbindgen crate来生成相应的C语言header文件,build.rs内容可为:

extern crate cbindgen;
    


    use std::env;
    use std::path::PathBuf;
    


    fn main() {
        let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
        let target_dir = get_target_dir(&crate_dir);
        let profile = env::var("PROFILE").unwrap();
        let package_name = env::var("CARGO_PKG_NAME").unwrap();
        let output_file_buf = target_dir.join(profile).join(format!("{}.h", package_name));
        let output_file = output_file_buf.to_str().unwrap();
    


        let config = cbindgen::Config {
            language: cbindgen::Language::C,
            ..Default::default()
        };
    


        match cbindgen::generate_with_config(&crate_dir, config) {
            Err(e) => println!("{}", e),
            Ok(v) => {
                v.write_to_file(&output_file);
            }
        }
    }
    


    fn get_target_dir(crate_dir: &str) -> PathBuf {
        match env::var("CARGO_TARGET_DIR") {
            Ok(d) => PathBuf::from(d),
            Err(_) => PathBuf::from(crate_dir).join("target"),
        }
    }

3.5 如何将byte array 由Rust函数传递给FFI C接口?

具体参考 How to return byte array from Rust function to FFI C?中例子有:

#[repr(C)]
struct Buffer {
    data: *mut u8,
    len: usize,
}


extern "C" fn generate_data() -> Buffer {
    let mut buf = vec![0; 512].into_boxed_slice();
    let data = buf.as_mut_ptr();
    let len = buf.len();
    std::mem::forget(buf);
    Buffer { data, len }
}


extern "C" fn free_buf(buf: Buffer) {
    let s = unsafe { std::slice::from_raw_parts_mut(buf.data, buf.len) };
    //let s = s.as_mut_ptr();
    unsafe {
        Box::from_raw(s as *mut [u8]);
    }
}

3.5.1 Rust 数组与指针相互转换

参考知乎博文 Rust 数组与指针(一),举例:

fn main() {
    let array: [i32; 3] = [1, 2, 3];


    println!("{:?}", array); // [1, 2, 3]


    let ptr: *mut i32 = &array as *const [i32; 3] as *mut i32;


    unsafe {
        println!("{:?}", ptr.add(0).read()); // 1


        ptr.add(1).write(456); // 第2个元素
    }


    println!("{:?}", array); // [1, 456, 3]


    let s = unsafe { std::slice::from_raw_parts_mut(ptr, 3) };
    println!("{:?}", s[0]); // 1
    s[1] = 789;
    println!("{:?}", array); // [1, 789, 3]
}

参考资料:

[1] 张汉东书籍《Rust编程之道》

[2] Rust FFI: rust语言和其他语言的相互调用

[3] https://github.com/alexcrichton/rust-ffi-examples

[4] 从Rust库中公开FFI

[5] https://doc.rust-lang.org/nomicon/other-reprs.html

[6] https://doc.rust-lang.org/cargo/reference/build-scripts.html

[7] How to return byte array from Rust function to FFI C?

[8] Rust 数组与指针(一)

————————————————

版权声明:本文为CSDN博主「mutourend」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/mutourend/article/details/106531670