Rust FFI编程:与C/C++及其他语言的无缝集成
【免费下载链接】rust 赋能每个人构建可靠且高效的软件。 项目地址: https://gitcode.com/GitHub_Trending/ru/rust
你是否曾想在Rust项目中复用现有的C/C++库,或者需要让Rust代码被其他语言调用?Rust的FFI(Foreign Function Interface,外部函数接口)功能让这一切变得简单。本文将带你一步步掌握Rust与C/C++及其他语言的无缝集成,解决跨语言调用的痛点问题。读完本文,你将能够:
- 理解Rust FFI的基本概念和工作原理
- 掌握Rust与C语言之间的数据类型转换
- 学会在Rust中调用C函数和在C中调用Rust函数
- 了解FFI中的内存安全和最佳实践
什么是FFI?
FFI(Foreign Function Interface,外部函数接口)是编程语言提供的一种机制,允许不同语言编写的代码相互调用。在Rust中,FFI主要用于与C语言及其衍生语言(如C++、Objective-C等)进行交互。Rust的FFI实现遵循C语言的调用约定,这使得它能够与任何支持C接口的语言进行互操作。
Rust标准库中提供了专门的模块来支持FFI编程。核心功能定义在std::ffi模块中,其中包含了处理C字符串的CString和CStr类型,以及其他与C语言交互的工具。
// FFI相关模块定义
// 源码位置: [library/alloc/src/ffi/mod.rs](https://gitcode.com/GitHub_Trending/ru/rust/blob/15283f6fe95e5b604273d13a428bab5fc0788f5a/library/alloc/src/ffi/mod.rs?utm_source=gitcode_repo_files)
//! Utilities related to FFI bindings.
//!
//! This module provides utilities to handle data across non-Rust
//! interfaces, like other programming languages and the underlying
//! operating system. It is mainly of use for FFI (Foreign Function
//! Interface) bindings and code that needs to exchange C-like strings
//! with other languages.
Rust与C类型的对应关系
在进行FFI编程时,首先需要了解Rust类型与C类型之间的对应关系。Rust标准库的std::os::raw模块定义了与C语言基本类型对应的Rust类型:
| C类型 | Rust类型 | 说明 |
|---|---|---|
char | c_char | C字符类型 |
short | c_short | 短整型 |
int | c_int | 整型 |
long | c_long | 长整型 |
long long | c_longlong | 长长整型 |
float | c_float | 单精度浮点型 |
double | c_double | 双精度浮点型 |
void * | *mut c_void | 通用指针 |
除了基本类型,Rust还提供了用于处理C风格字符串的类型:CString和CStr。
CString表示一个拥有所有权的C兼容字符串,它确保字符串中不包含内部 nul 字节('\0'),并在末尾添加一个 nul 终止符。CStr则表示一个借用的C字符串,用于包装从C代码获取的字符串指针。
// CString结构定义
// 源码位置: [library/alloc/src/ffi/c_str.rs](https://gitcode.com/GitHub_Trending/ru/rust/blob/15283f6fe95e5b604273d13a428bab5fc0788f5a/library/alloc/src/ffi/c_str.rs?utm_source=gitcode_repo_files)
/// A type representing an owned, C-compatible, nul-terminated string with no nul bytes in the
/// middle.
///
/// This type serves the purpose of being able to safely generate a
/// C-compatible string from a Rust byte slice or vector. An instance of this
/// type is a static guarantee that the underlying bytes contain no interior 0
/// bytes ("nul characters") and that the final byte is 0 ("nul terminator").
#[derive(PartialEq, PartialOrd, Eq, Ord, Hash, Clone)]
#[stable(feature = "alloc_c_string", since = "1.64.0")]
pub struct CString {
// Invariant 1: the slice ends with a zero byte and has a length of at least one.
// Invariant 2: the slice contains only one zero byte.
// Improper usage of unsafe function can break Invariant 2, but not Invariant 1.
inner: Box<[u8]>,
}
在Rust中调用C函数
要在Rust中调用C函数,需要使用extern "C"块来声明外部函数。这告诉Rust编译器这些函数遵循C语言的调用约定。
基本步骤
- 使用
extern "C"块声明C函数原型 - 在Cargo.toml中配置链接选项,指定要链接的C库
- 在Rust代码中调用声明的C函数
示例:调用C标准库函数
下面是一个调用C标准库puts函数的简单示例:
// 使用extern "C"块声明C函数
extern "C" {
fn puts(s: *const std::os::raw::c_char);
}
fn main() {
// 创建C兼容字符串
let c_str = std::ffi::CString::new("Hello from Rust FFI!").expect("Failed to create CString");
// 调用C函数(需要在unsafe块中)
unsafe {
puts(c_str.as_ptr());
}
}
在这个例子中,我们首先声明了C标准库中的puts函数。然后,我们使用CString::new创建了一个C兼容的字符串。最后,在unsafe块中调用了puts函数,传递了字符串的原始指针。
注意:所有FFI调用都需要放在
unsafe块中,因为Rust无法保证外部函数会遵守Rust的内存安全规则。
处理C字符串
Rust提供了CString和CStr两种类型来处理C风格的字符串:
CString:拥有所有权的C字符串,用于将Rust字符串传递给C函数CStr:借用的C字符串,用于处理从C函数接收的字符串
use std::ffi::{CString, CStr};
use std::os::raw::c_char;
// 声明C函数
extern "C" {
fn get_c_string() -> *const c_char;
fn free_c_string(s: *mut c_char);
}
fn main() {
unsafe {
// 调用C函数获取字符串指针
let c_str_ptr = get_c_string();
// 将原始指针包装为CStr
let c_str = CStr::from_ptr(c_str_ptr);
// 转换为Rust字符串
if let Ok(rust_str) = c_str.to_str() {
println!("Received from C: {}", rust_str);
}
// 释放C字符串内存(如果C函数要求的话)
free_c_string(c_str_ptr as *mut c_char);
}
}
在C中调用Rust函数
除了在Rust中调用C函数,我们还可以将Rust函数暴露给C语言调用。这需要使用#[no_mangle]属性来禁止Rust编译器对函数名进行混淆,并使用extern "C"来指定C调用约定。
基本步骤
- 使用
#[no_mangle]和extern "C"属性导出Rust函数 - 编译Rust代码为动态链接库或静态库
- 在C代码中声明函数原型并调用
示例:导出Rust函数给C调用
// lib.rs
use std::ffi::{CString, CStr};
use std::os::raw::c_char;
// 导出Rust函数给C使用
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
// 处理字符串的函数
#[no_mangle]
pub extern "C" fn rust_greet(name: *const c_char) -> *mut c_char {
// 将C字符串转换为Rust字符串
let c_str = unsafe { CStr::from_ptr(name) };
let name = match c_str.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
// 创建问候语
let greeting = format!("Hello, {}!", name);
// 将Rust字符串转换为C字符串并返回
match CString::new(greeting) {
Ok(c_string) => c_string.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
// 释放由rust_greet分配的内存
#[no_mangle]
pub extern "C" fn rust_free_string(s: *mut c_char) {
unsafe {
if !s.is_null() {
CString::from_raw(s);
}
}
}
在这个例子中,我们定义了三个可以被C调用的Rust函数:
rust_add:简单的整数加法函数rust_greet:接收C字符串,返回新分配的C字符串rust_free_string:释放由rust_greet分配的内存
注意:当Rust函数返回拥有所有权的C字符串时,需要提供一个对应的释放函数,以便C代码可以正确释放内存,避免内存泄漏。
在C中调用Rust函数
下面是一个在C代码中调用上述Rust函数的示例:
// main.c
#include <stdio.h>
#include <stdlib.h>
// 声明Rust导出的函数
extern int rust_add(int a, int b);
extern char* rust_greet(const char* name);
extern void rust_free_string(char* s);
int main() {
// 调用简单的加法函数
int result = rust_add(2, 3);
printf("2 + 3 = %d\n", result);
// 调用字符串处理函数
const char* name = "C Programmer";
char* greeting = rust_greet(name);
if (greeting != NULL) {
printf("%s\n", greeting);
rust_free_string(greeting); // 释放内存
}
return 0;
}
FFI中的内存管理
内存管理是FFI编程中最容易出错的部分。由于Rust和C使用不同的内存分配器,因此需要特别注意内存的所有权和释放问题。
内存所有权规则
- 谁分配,谁释放:通常情况下,应该由分配内存的语言负责释放内存
- 避免跨语言传递复杂数据结构的所有权
- 使用原始指针进行跨语言数据传递
传递复杂数据结构
当需要传递复杂数据结构时,可以使用#[repr(C)]属性来确保结构体的内存布局与C兼容。
// 使用#[repr(C)]确保结构体布局与C兼容
#[repr(C)]
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
// 导出Rust函数操作Point结构体
#[no_mangle]
pub extern "C" fn create_point(x: f64, y: f64) -> *mut Point {
let point = Box::new(Point { x, y });
Box::into_raw(point) // 释放Box的所有权,返回原始指针
}
#[no_mangle]
pub extern "C" fn distance(p1: *const Point, p2: *const Point) -> f64 {
unsafe {
// 解引用原始指针(不安全操作)
let p1 = &*p1;
let p2 = &*p2;
let dx = p1.x - p2.x;
let dy = p1.y - p2.y;
(dx*dx + dy*dy).sqrt()
}
}
#[no_mangle]
pub extern "C" fn free_point(p: *mut Point) {
unsafe {
// 将原始指针重新包装为Box,然后自动释放
if !p.is_null() {
Box::from_raw(p);
}
}
}
FFI最佳实践
使用bindgen自动生成绑定
对于大型C库,手动编写所有FFI绑定既繁琐又容易出错。可以使用bindgen工具自动从C头文件生成Rust绑定。
在Cargo.toml中添加依赖:
[build-dependencies]
bindgen = "0.64"
创建build.rs文件:
// build.rs
extern crate bindgen;
use std::path::PathBuf;
fn main() {
// 生成绑定
let bindings = bindgen::Builder::default()
.header("wrapper.h") // 包含C头文件
.generate()
.expect("Failed to generate bindings");
// 将绑定写入文件
let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Failed to write bindings");
}
使用libc crate
libc crate提供了完整的C标准库类型和函数的Rust绑定,避免手动声明的麻烦。
在Cargo.toml中添加依赖:
[dependencies]
libc = "0.2"
使用示例:
use libc::{c_char, puts};
use std::ffi::CString;
fn main() {
let msg = CString::new("Hello from libc!").unwrap();
unsafe {
puts(msg.as_ptr() as *const c_char);
}
}
错误处理
在FFI边界传递错误信息需要特别注意,因为Rust的Result类型在C中没有直接对应的类型。通常的做法是:
- 使用整数错误码
- 提供额外的函数来获取详细错误信息
- 使用输出参数传递错误信息
use std::ffi::{CString, CStr};
use std::os::raw::c_char;
// 错误码
#[repr(C)]
pub enum ErrorCode {
SUCCESS,
INVALID_INPUT,
MEMORY_ERROR,
}
// 导出函数,使用错误码返回错误
#[no_mangle]
pub extern "C" fn process_input(input: *const c_char, output: *mut *mut c_char) -> ErrorCode {
if input.is_null() || output.is_null() {
return ErrorCode::INVALID_INPUT;
}
let c_str = unsafe { CStr::from_ptr(input) };
let rust_str = match c_str.to_str() {
Ok(s) => s,
Err(_) => return ErrorCode::INVALID_INPUT,
};
// 处理输入...
let result = format!("Processed: {}", rust_str);
// 创建输出C字符串
let c_result = match CString::new(result) {
Ok(s) => s,
Err(_) => return ErrorCode::MEMORY_ERROR,
};
unsafe {
*output = c_result.into_raw();
}
ErrorCode::SUCCESS
}
// 释放输出字符串的函数
#[no_mangle]
pub extern "C" fn free_output(str_ptr: *mut c_char) {
if !str_ptr.is_null() {
unsafe {
CString::from_raw(str_ptr);
}
}
}
高级主题:Rust与C++交互
虽然Rust的FFI直接支持C语言,但通过一些额外的工作,也可以与C++代码交互。主要方法有:
- 使用
extern "C"包装C++函数,提供C兼容接口 - 使用bindgen生成C++标准库的绑定
- 使用专门的crates如
cxx提供更安全的C++交互
使用cxx crate
cxx是一个强大的Rust库,提供了类型安全的Rust-C++互操作,无需手动编写不安全的FFI代码。
在Cargo.toml中添加依赖:
[dependencies]
cxx = "1.0"
使用示例:
// src/main.rs
use cxx::CxxString;
// 声明C++函数
#[cxx::bridge]
mod ffi {
extern "C++" {
fn cpp_function(name: &str) -> CxxString;
}
}
fn main() {
let result = ffi::cpp_function("Rust");
println!("Result from C++: {}", result);
}
总结
Rust的FFI机制为与其他语言交互提供了强大而灵活的能力。通过本文的介绍,你应该已经掌握了Rust与C/C++交互的基本技能:
- 理解了Rust FFI的基本概念和工作原理
- 学会了Rust与C类型之间的转换,特别是字符串处理
- 掌握了在Rust中调用C函数和在C中调用Rust函数的方法
- 了解了FFI中的内存管理最佳实践和错误处理技巧
FFI编程虽然强大,但也充满了潜在的危险。始终记住在unsafe块中进行FFI调用,并仔细管理跨语言的内存所有权。通过遵循本文介绍的最佳实践,你可以安全有效地利用Rust的FFI能力,构建跨语言的应用程序。
Rust的FFI功能不仅限于与C/C++交互,还可以通过C接口与许多其他语言(如Python、Java、C#等)进行交互。这使得Rust成为一个优秀的系统级编程语言,可以无缝集成到现有的软件生态系统中。
要深入了解Rust FFI,可以查阅官方文档:
【免费下载链接】rust 赋能每个人构建可靠且高效的软件。 项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



