MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust C ABI兼容性与跨语言调用

2023-09-207.2k 阅读

Rust 与 C ABI 的基础概念

  1. 什么是 ABI 应用二进制接口(Application Binary Interface,ABI)定义了不同编译单元(通常是不同语言编写的模块)在二进制层面如何相互交互。它涵盖了诸如函数调用约定(如何传递参数、返回值)、数据类型表示(结构体布局、对齐方式)等细节。对于跨语言调用,ABI 的兼容性至关重要,因为不同语言的编译器在这些方面可能有不同的默认设置。
  2. C ABI 的特点 C 语言作为一种广泛使用且具有较高可移植性的语言,其 ABI 被众多其他语言所采用或兼容。C ABI 的函数调用约定相对简单和通用。例如,在 x86 - 64 架构上,通常前 6 个整数或指针类型参数通过寄存器传递(rdirsirdxrcxr8r9),其他参数通过栈传递。返回值通常放在rax寄存器中。这种一致性使得 C 语言编写的库能够被多种语言调用,如 C++、Python(通过 CFFI 等工具)、Rust 等。
  3. Rust 对 C ABI 的支持 Rust 语言设计上就考虑了与 C ABI 的兼容性,这使得 Rust 编写的代码可以很方便地与 C 语言代码相互调用。Rust 通过extern "C"关键字来指定函数使用 C ABI。例如:
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

在这个例子中,extern "C"表明add函数遵循 C ABI,#[no_mangle]属性防止 Rust 对函数名进行重整(name mangling),以确保在链接时能按 C 语言的命名规则找到该函数。

Rust 函数调用 C ABI 函数

  1. 调用 C 标准库函数 Rust 可以轻松调用 C 标准库函数。例如,要调用printf函数,可以这样做:
extern crate libc;
use libc::c_char;
use std::ffi::CString;

fn main() {
    let message = CString::new("Hello, world!").expect("Failed to create CString");
    unsafe {
        libc::printf(b"%s\0".as_ptr() as *const c_char, message.as_ptr());
    }
}

首先,通过extern crate libc引入 Rust 对 C 标准库的绑定。libc库提供了对 C 标准库函数和类型的 Rust 接口。CString用于将 Rust 的String转换为适合 C ABI 的以空字符结尾的字符串。然后,在unsafe块中调用libc::printf函数,因为直接调用外部 C 函数可能会违反 Rust 的安全规则(如内存安全)。 2. 调用自定义 C ABI 函数 假设我们有一个用 C 语言编写的简单库,math.c

#include <stdint.h>

int32_t multiply(int32_t a, int32_t b) {
    return a * b;
}

编译成动态库libmath.so(在 Linux 下)。在 Rust 中调用这个函数:

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

fn main() {
    unsafe {
        let result = multiply(3, 4);
        println!("The result of multiplication is: {}", result);
    }
}

这里通过extern "C"块声明了要调用的 C 函数multiply。在main函数的unsafe块中调用该函数,获取并打印乘法结果。

C 函数调用 Rust C ABI 函数

  1. 编写 Rust 库供 C 调用 创建一个 Rust 库项目,在lib.rs中编写如下代码:
#[no_mangle]
pub extern "C" fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

然后通过cargo build --release构建库。在 Linux 下,会生成liblibname.solibname是项目名称)。 2. 从 C 中调用 Rust 库函数 编写一个 C 程序main.c来调用上述 Rust 库函数:

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

// 声明要调用的 Rust 函数
int32_t subtract(int32_t a, int32_t b);

int main() {
    int32_t result = subtract(5, 3);
    printf("The result of subtraction is: %d\n", result);
    return 0;
}

编译并链接:

gcc -o main main.c -L/path/to/rust/lib -llibname

这里-L指定 Rust 库的路径,-llibname指定要链接的 Rust 库名。运行main程序,就可以看到调用 Rust 函数的结果。

Rust 结构体与 C ABI 兼容性

  1. 简单 Rust 结构体映射到 C ABI Rust 结构体在与 C ABI 交互时,需要注意布局和对齐。对于简单的结构体,可以使用repr(C)属性来确保其布局与 C 结构体一致。例如:
#[repr(C)]
pub struct Point {
    x: i32,
    y: i32,
}

#[no_mangle]
pub extern "C" fn distance(p1: &Point, p2: &Point) -> f64 {
    let dx = (p1.x - p2.x) as f64;
    let dy = (p1.y - p2.y) as f64;
    (dx * dx + dy * dy).sqrt()
}

#[repr(C)]告诉 Rust 编译器按照 C 语言的规则来布局Point结构体。这样,在 C 语言中可以定义相同布局的结构体来与 Rust 进行交互。 2. C 结构体与 Rust 结构体的相互转换 假设我们有一个 C 结构体Rectangle

#include <stdint.h>

typedef struct {
    int32_t width;
    int32_t height;
} Rectangle;

在 Rust 中定义对应的结构体并实现相互转换:

#[repr(C)]
pub struct Rectangle {
    pub width: i32,
    pub height: i32,
}

impl From<&Rectangle> for Rectangle {
    fn from(rect: &Rectangle) -> Self {
        Rectangle {
            width: rect.width,
            height: rect.height,
        }
    }
}

#[no_mangle]
pub extern "C" fn area(rect: &Rectangle) -> i32 {
    rect.width * rect.height
}

在 C 中调用:

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

// 声明要调用的 Rust 函数
int32_t area(const Rectangle* rect);

int main() {
    Rectangle rect = {4, 5};
    int32_t result = area(&rect);
    printf("The area of the rectangle is: %d\n", result);
    return 0;
}

这里通过repr(C)保证了结构体布局一致,并且 Rust 实现了从 C 风格结构体引用到 Rust 结构体的转换,方便在 Rust 函数中使用。

Rust 枚举与 C ABI 兼容性

  1. Rust 枚举映射到 C ABI Rust 枚举在与 C ABI 交互时,也需要特别处理。使用repr(C)属性可以将 Rust 枚举映射到 C 兼容的类型。例如:
#[repr(C)]
pub enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

#[no_mangle]
pub extern "C" fn print_color(color: Color) {
    match color {
        Color::Red => println!("Red"),
        Color::Green => println!("Green"),
        Color::Blue => println!("Blue"),
    }
}

在 C 语言中,可以定义一个等价的枚举类型:

#include <stdio.h>

typedef enum {
    Red = 0,
    Green = 1,
    Blue = 2
} Color;

// 声明要调用的 Rust 函数
void print_color(Color color);

int main() {
    Color c = Blue;
    print_color(c);
    return 0;
}
  1. 处理 C 风格枚举在 Rust 中的使用 如果有一个 C 风格的枚举定义在enum_example.h中:
#ifndef ENUM_EXAMPLE_H
#define ENUM_EXAMPLE_H

typedef enum {
    OptionA = 1,
    OptionB = 2,
    OptionC = 3
} Option;

#endif

在 Rust 中使用这个枚举:

extern crate libc;
use libc::c_int;

#[repr(C)]
pub enum Option {
    OptionA = 1,
    OptionB = 2,
    OptionC = 3,
}

#[no_mangle]
pub extern "C" fn handle_option(opt: Option) {
    match opt {
        Option::OptionA => println!("Handling OptionA"),
        Option::OptionB => println!("Handling OptionB"),
        Option::OptionC => println!("Handling OptionC"),
    }
}

然后在 C 中可以调用handle_option函数来处理枚举值。

复杂数据类型与 C ABI 兼容性

  1. 字符串处理 在 Rust 与 C ABI 交互中,字符串处理是一个常见的问题。Rust 的String类型与 C 的以空字符结尾的字符串(char*)有不同的表示方式。如前面提到的,将 Rust 的String转换为 C 字符串可以使用CString。例如,将 Rust 字符串传递给 C 函数并返回结果:
extern crate libc;
use libc::c_char;
use std::ffi::CString;

extern "C" {
    fn process_string(str: *const c_char) -> *mut c_char;
}

fn main() {
    let input = CString::new("Hello, C!").expect("Failed to create CString");
    unsafe {
        let result = process_string(input.as_ptr());
        let result_str = CString::from_raw(result);
        println!("Result from C: {}", result_str.to_str().unwrap());
    }
}

在 C 中实现process_string函数:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* process_string(const char* str) {
    size_t len = strlen(str);
    char* new_str = (char*)malloc(len + 7);
    if (new_str == NULL) {
        return NULL;
    }
    sprintf(new_str, "Processed: %s", str);
    return new_str;
}
  1. 动态数组与指针 在 Rust 中,Vec是动态数组,而在 C 中通常使用指针和手动内存管理来实现动态数组。例如,将 Rust 的Vec传递给 C 函数:
extern crate libc;
use libc::c_int;
use std::ptr;

extern "C" {
    fn sum_array(arr: *const c_int, len: c_int) -> c_int;
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let len = numbers.len() as c_int;
    let ptr = numbers.as_ptr();
    unsafe {
        let result = sum_array(ptr, len);
        println!("Sum of array: {}", result);
    }
}

在 C 中实现sum_array函数:

#include <stdint.h>

int32_t sum_array(const int32_t* arr, int32_t len) {
    int32_t sum = 0;
    for (int32_t i = 0; i < len; i++) {
        sum += arr[i];
    }
    return sum;
}

这里展示了如何在 Rust 和 C 之间传递动态数组,并在 C 中对其进行操作。

跨语言调用中的错误处理

  1. Rust 调用 C 函数的错误处理 当 Rust 调用 C 函数时,C 函数可能通过返回错误码来表示错误。例如,open函数在 C 中如果打开文件失败会返回-1。在 Rust 中调用open
extern crate libc;
use libc::{c_int, open, O_RDONLY};
use std::ffi::CString;
use std::io::{Error, ErrorKind};

fn open_file(file_path: &str) -> Result<c_int, Error> {
    let c_path = CString::new(file_path).expect("Failed to create CString");
    let fd = unsafe { open(c_path.as_ptr(), O_RDONLY) };
    if fd == -1 {
        Err(Error::last_os_error())
    } else {
        Ok(fd)
    }
}

这里通过检查返回值并使用std::io::Error来处理可能的错误。 2. C 调用 Rust 函数的错误处理 在 Rust 中,我们可以通过返回特定的错误码或者使用Result类型来向 C 调用者传达错误。例如:

#[repr(C)]
pub enum ErrorCode {
    Success = 0,
    InvalidInput = 1,
    OtherError = 2,
}

#[no_mangle]
pub extern "C" fn divide(a: i32, b: i32, result: *mut i32) -> ErrorCode {
    if b == 0 {
        ErrorCode::InvalidInput
    } else {
        unsafe {
            *result = a / b;
        }
        ErrorCode::Success
    }
}

在 C 中调用:

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

typedef enum {
    Success = 0,
    InvalidInput = 1,
    OtherError = 2
} ErrorCode;

// 声明要调用的 Rust 函数
ErrorCode divide(int32_t a, int32_t b, int32_t* result);

int main() {
    int32_t result;
    ErrorCode code = divide(10, 2, &result);
    if (code == Success) {
        printf("Result: %d\n", result);
    } else if (code == InvalidInput) {
        printf("Invalid input: division by zero\n");
    } else {
        printf("Other error\n");
    }
    return 0;
}

通过这种方式,Rust 函数可以向 C 调用者传递错误信息。

Rust 与其他语言通过 C ABI 交互

  1. Rust 与 Python 通过 C ABI 交互 Python 可以通过 CFFI 库与 Rust 进行交互。首先,编写一个 Rust 库:
#[no_mangle]
pub extern "C" fn power(base: f64, exponent: f64) -> f64 {
    base.powf(exponent)
}

构建库后,在 Python 中使用 CFFI:

from cffi import FFI

ffi = FFI()
ffi.cdef("double power(double base, double exponent);")
lib = ffi.dlopen('path/to/libname.so')

result = lib.power(2.0, 3.0)
print(f"2.0 ^ 3.0 = {result}")

这里通过 CFFI 定义了 Rust 函数的接口,并加载 Rust 库进行调用。 2. Rust 与 Java 通过 C ABI 交互 在 Java 中,可以通过 JNI(Java Native Interface)与 Rust 进行交互。编写一个 Rust 函数:

#[no_mangle]
pub extern "C" fn greet() -> *const u8 {
    let message = "Hello from Rust!";
    message.as_ptr()
}

在 Java 中使用 JNI 调用:

public class RustJNIExample {
    static {
        System.loadLibrary("name");
    }

    private native static long greet();

    public static void main(String[] args) {
        long messagePtr = greet();
        String message = Native.getString(messagePtr);
        System.out.println(message);
    }
}

这里通过 JNI 加载 Rust 库并调用函数,虽然实际应用中还需要处理更多细节,如内存管理等,但基本展示了通过 C ABI 实现 Rust 与 Java 交互的流程。

通过上述内容,详细介绍了 Rust 与 C ABI 的兼容性以及跨语言调用的各个方面,包括函数调用、结构体和枚举的处理、复杂数据类型交互、错误处理以及与其他语言如 Python 和 Java 的交互。希望这些内容能帮助开发者更好地利用 Rust 进行跨语言开发。