在实际开发中,我们常会遇到需要复用C语言成熟库的场景。Rust凭借FFI(Foreign Function Interface)机制,能轻松实现与C语言的交互,无需重写或移植现有C代码。本文就基于 Ubuntu 22.04 环境,一步步拆解Rust调用C函数的完整流程,从简单函数到复杂结构体指针传递,覆盖核心用法与实操细节。
在这里插入图片描述

一、FFI核心原理与前置知识

1. FFI存在的意义

不同编程语言编写的代码库往往各有优势,当Rust项目需要使用C语言库时,无需从零重写,通过 FFI即可直接调用C函数,大幅提升开发效率。

2. 调用的底层基础

所有语言编译后都会转化为CPU可执行的二进制指令,这是跨语言调用的前提。而 ABI(二进制接口)作为统一的调用约定,定义了参数传递、类型表示和名称修饰规则,确保不同语言生成的二进制代码能相互识别。

3.Rust与C的 ABI 适配

绝大多数C库遵循cdecl调用约定,Rust原生支持包括cdecl在内的主流 ABI 规范,这为Rust调用C函数提供了直接支持。同时Rust涵盖了所有C语言基础数据类型,比如C的 int 对应Rust的 i32,C 的 double 对应Rust的 f64,为类型转换提供了便利。

二、Rust调用C函数的三种场景实操

1. 调用C标准库函数

C 标准库提供了大量基础工具函数,下面以字符串长度计算(strlen)、整数取模(fmod)和字符大小写转换(toupper)为例,演示Rust如何直接调用。

首先在Rust代码中通过 extern "C" 声明要调用的C函数,再在 unsafe 块中执行调用(因外部调用可能存在安全风险,Rust要求显式标记不安全区域):

use std::ffi::{CString, CStr};
use std::os::raw::{c_char, c_double, c_int, c_uchar};

// 声明C标准库中的三个函数
extern "C" {
    // 计算字符串长度(不含终止符 '\0')
    fn strlen(s: *const c_char) -> c_uchar;
    // 计算两数取模(x mod y)
    fn fmod(x: c_double, y: c_double) -> c_double;
    // 字符转大写(仅对小写字母有效)
    fn toupper(c: c_int) -> c_int;
}

fn main() {
    // 测试 strlen:计算字符串长度
    letRust_str = CString::new("HelloRustFFI").unwrap();
    let str_len = unsafe { strlen(rust_str.as_ptr()) };
    println!("字符串 \"{}\" 的长度是: {}",Rust_str.to_str().unwrap(), str_len);

    // 测试 fmod:计算浮点数取模
    let num1: f64 = 10.5;
    let num2: f64 = 3.2;
    let mod_result = unsafe { fmod(num1, num2) };
    println!("{} 对 {} 取模的结果是: {:.2}", num1, num2, mod_result);

    // 测试 toupper:字符转大写
    let lower_c = b'a' as c_int;  // 小写字母 'a' 的 ASCII 码
    let upper_c = unsafe { toupper(lower_c) } as u8 as char;
    println!("字符 'a' 转大写的结果是: '{}'", upper_c);

    let non_lower_c = b'5' as c_int;  // 非小写字母(数字 5)
    let non_upper_c = unsafe { toupper(non_lower_c) } as u8 as char;
    println!("字符 '5' 转大写的结果是: '{}'", non_upper_c);
}

运行结果:

字符串 "HelloRustFFI" 的长度是: 14
10.5 对 3.2 取模的结果是: 0.90
字符 'a' 转大写的结果是: 'A'
字符 '5' 转大写的结果是: '5'

2. 调用自定义C库

当需要调用自己编写的C函数时,需先编译C代码为静态库,再让Rust项目链接使用。

步骤 1:搭建项目结构

创建如下项目目录:

rust-call-c
├── build.rs
├── Cargo.toml
├── c_src
│   └── greet.c
└── src
    └── main.rs
步骤 2:编写自定义C函数

c_src/greet.c 中实现简单的打印函数:

#include <stdio.h>

void say_hello(const char* name) {
    printf("Hi %s! Welcome toRustFFIcalling C!\n", name);
}
步骤 3:配置构建脚本

build.rs 中指定C源文件路径,确保编译Rust时自动编译C代码:

fn main() {
    //C文件修改时自动重新构建
    println!("cargo:rerun-if-changed=c_src/greet.c");
    
    // 编译C代码为静态库
    cc::Build::new()
        .file("c_src/greet.c")
        .compile("greet_lib");
}
步骤 4:配置 Cargo.toml

添加构建依赖 cc,用于编译C代码:

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

[build-dependencies]
cc = "1.0"
步骤 5:Rust调用自定义C函数

src/main.rs 中声明并调用C函数:

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

// 声明自定义C库中的函数
extern "C" {
    fn say_hello(name: *const c_char);
}

fn main() {
    // 将Rust字符串转换为C兼容字符串
    let username = CString::new("RustDeveloper").unwrap();
    
    // 调用自定义C函数
    unsafe {
        say_hello(username.as_ptr());
    }
    println!("Rustside: Call toCfunction completed!");
}

运行结果:

HiRustDeveloper! Welcome toRustFFIcalling C!
Rustside: Call toCfunction completed!

3. 调用带结构体指针的复杂C函数

当C函数需要传递结构体指针时,手动处理类型转换较为繁琐,可使用 bindgen 工具自动生成Rust绑定代码,简化开发。下面以自定义的“学生信息处理”C 函数为例,演示完整流程。

步骤 1:编写自定义C头文件与源文件

首先创建自定义C代码,实现学生信息的初始化和打印功能。

创建 c_src/student.h 头文件:

#ifndef STUDENT_H
#define STUDENT_H

// 学生信息结构体
struct Student {
    int id;               // 学号
    const char* name;     // 姓名
    float score;          // 成绩
    int age;              // 年龄
};

// 初始化学生信息
void init_student(struct Student* stu, int id, const char* name, float score, int age);

// 打印学生信息
void print_student(const struct Student* stu);

#endif

创建 c_src/student.c 源文件:

#include "student.h"
#include <stdio.h>

// 初始化学生信息
void init_student(struct Student* stu, int id, const char* name, float score, int age) {
    if (stu != NULL) {
        stu->id = id;
        stu->name = name;
        stu->score = score;
        stu->age = age;
    }
}

// 打印学生信息
void print_student(const struct Student* stu) {
    if (stu != NULL) {
        printf("Student Info:\n");
        printf("ID: %d\n", stu->id);
        printf("Name: %s\n", stu->name);
        printf("Age: %d\n", stu->age);
        printf("Score: %.2f\n", stu->score);
    } else {
        printf("Invalid student pointer!\n");
    }
}
步骤 2:安装 bindgen 工具与依赖
# 安装系统依赖(Debian/Ubuntu 系列)
sudo apt install llvm-dev libclang-dev clang

# 安装 bindgen 命令行工具
cargo install bindgen-cli
步骤 3:生成C头文件的Rust绑定

在Rust项目根目录执行命令,根据 student.h 生成Rust绑定代码:

bindgen c_src/student.h > src/student_bind.rs

生成的 src/student_bind.rs 中,会自动包含 Student 结构体和两个函数的Rust声明,核心内容如下:

extern "C" {
    pub fn init_student(
        stu: *mut Student,
        id: ::std::os::raw::c_int,
        name: *const ::std::os::raw::c_char,
        score: ::std::os::raw::c_float,
        age: ::std::os::raw::c_int,
    );
    pub fn print_student(stu: *const Student);
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Student {
    pub id: ::std::os::raw::c_int,
    pub name: *const ::std::os::raw::c_char,
    pub score: ::std::os::raw::c_float,
    pub age: ::std::os::raw::c_int,
}
步骤 4:配置构建脚本与 Cargo.toml

修改 build.rs,确保编译Rust时同步编译自定义C代码:

fn main() {
    //C文件修改时自动重新构建
    println!("cargo:rerun-if-changed=c_src/student.c");
    println!("cargo:rerun-if-changed=c_src/student.h");
    
    // 编译C代码为静态库
    cc::Build::new()
        .file("c_src/student.c")
        .include("c_src")  // 指定头文件目录
        .compile("student_lib");
}

更新 Cargo.toml,保留构建依赖:

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

[build-dependencies]
cc = "1.0"
步骤 5:Rust调用带结构体指针的C函数

src/main.rs 中引入绑定代码,构造结构体并调用C函数:

use std::ffi::CString;
use std::os::raw::{c_char, c_float, c_int};

// 引入自动生成的绑定代码
mod student_bind;

fn main() {
    // 1. 创建C兼容的字符串(学生姓名)
    let student_name = CString::new("Zhang San").unwrap();

    // 2. 初始化Rust侧的 Student 结构体(对应C的 struct Student)
    let mutRust_student = student_bind::Student {
        id: 0,
        name: std::ptr::null(),  // 初始化为空指针
        score: 0.0,
        age: 0,
    };

    // 3. 调用C函数初始化学生信息
    unsafe {
        student_bind::init_student(
            &mutRust_student,          // 结构体指针
            2024001 as c_int,           // 学号
            student_name.as_ptr(),      // 姓名指针
            92.5 as c_float,            // 成绩
            20 as c_int                 // 年龄
        );
    }

    // 4. 调用C函数打印学生信息
    println!("CallingCfunction to print student info:");
    unsafe {
        student_bind::print_student(&rust_student);
    }

    // 5. 修改结构体字段后再次调用C函数
   Rust_student.score = 95.8 as c_float;
    println!("\nAfter updating score, callCfunction again:");
    unsafe {
        student_bind::print_student(&rust_student);
    }
}

运行结果:

CallingCfunction to print student info:
Student Info:
ID: 2024001
Name: Zhang San
Age: 20
Score: 92.50

After updating score, callCfunction again:
Student Info:
ID: 2024001
Name: Zhang San
Age: 20
Score: 95.80

三、注意事项与常见问题

  1. 所有C函数调用必须包裹在 unsafe 块中,因为外部函数可能违反Rust的安全约定。
  2. 类型转换需严格对应,Rust中需使用 std::os::raw 模块下的类型(如 c_intc_float)与C类型匹配。
  3. 字符串传递需通过 CStringCStr 进行转换,避免因字符串格式不兼容导致崩溃。
  4. 使用 bindgen 生成绑定代码时,可能会出现未使用函数或常量的告警,属于正常现象,无需处理。
  5. 传递可变对象或结构体指针时,需注意内存安全,避免出现野指针或双重释放问题。

想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐