Rust通过FFI调用C函数实战指南
在实际开发中,我们常会遇到需要复用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
三、注意事项与常见问题
- 所有C函数调用必须包裹在
unsafe块中,因为外部函数可能违反Rust的安全约定。 - 类型转换需严格对应,Rust中需使用
std::os::raw模块下的类型(如c_int、c_float)与C类型匹配。 - 字符串传递需通过
CString和CStr进行转换,避免因字符串格式不兼容导致崩溃。 - 使用
bindgen生成绑定代码时,可能会出现未使用函数或常量的告警,属于正常现象,无需处理。 - 传递可变对象或结构体指针时,需注意内存安全,避免出现野指针或双重释放问题。
想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)