类型
基础类型
Rust的标准库定了和C对应的类型
C type | Corresponding std::os::raw type |
---|---|
short | c_short |
int | c_int |
long | c_long |
long long | c_longlong |
unsigned short | c_ushort |
unsigned, unsigned int | c_uint |
unsigned long | c_ulong |
unsigned long long | c_ulonglong |
char | c_char |
signed char | c_schar |
unsigned char | c_uchar |
float | c_float |
double | c_double |
void , const void | mut c_void, const c_void |
- 除了c_void,其他的Rust类型都是Rust基础类型的别名,c_char是i8或u8。
- rust的bool和c的bool相同。
- rust的char和C的wchar_t不同,和char_32比较接近,但char_32不一定是Unicode。
- rust的usize,isize和C的size_t,ptrdiff_t有相同的表示
- C/C++的指针,C++的引用对应Rust的mut T和const T
- 理论上C允许使用Rust没有对应类型的表示,但现在所有的Rust实现都有对应的类型。
结构
C会保证内存里字段的顺和定义一样,但Rust一般会重新排序来最小化内存占用,而且还有不占内存的类型。定义结构体是可以用 #[repr(C)] 告诉Rust将结构体的内存模拟的C的内存结构。#[repr(C)]只影响整个结构体的内存结构,不影响字段的。所以每个字段还需要用和C对应的类型。
use std::os::raw::{c_char, c_int};
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int
}
对应C的
typedef struct {
char *message;
int klass;
}
枚举和union
定义C风格的枚举
#[repr(C)]
#[allow(non_camel_case_types)]
enum git_error_code {
GIT_OK = 0,
GIT_ERROR = -1,
GIT_ENOTFOUND = -3,
GIT_EEXISTS = -4,
...
}
一般Rust会用各种手段来选择枚举的表示方法,上面的例子一般Rust会只用一个字节来表示,但C会用一个int。
Rust还允许你指定enum的表示方法,用#[repr(i16)]可以让你定义一个等同于C++的这种枚举
#include <stdint.h>
enum git_error_code: int16_t {
GIT_OK = 0,
GIT_ERROR = -1,
GIT_ENOTFOUND = -3,
GIT_EEXISTS = -4,
};
使用#[repr(C)]来表示和使用的例子
enum tag {
FLOAT = 0,
INT = 1,
};
union number {
float f;
short i;
};
struct tagged_number {
tag t;
number n;
};
#[repr(C)]
enum Tag {
Float = 0,
Int = 1
}
#[repr(C)]
union FloatOrInt {
f: f32,
i: i32,
}
#[repr(C)]
struct Value {
tag: Tag,
union: FloatOrInt
}
fn is_zero(v: Value) -> bool {
use self::Tag::*;
unsafe {
match v {
Value { tag: Int, union: FloatOrInt { i: 0 } } => true,
Value { tag: Float, union: FloatOrInt { f: num } } => (num == 0.0),
_ => false
}
}
}
字符串
Rust和C的字符串之间有很多不同,C以null字符结尾,Rust存储了字符串的长度,而且能够吧null字符当一个元素存储。C字符串有可能包含错误的UTF-8字符。所以他们之间不能相互装换,因此Rust在std::ffi模块提供了对应的类型CString 和 CStr。
声明Foreign Functions和变量
函数
extern 代码段可以用来声明将要用到的C的函数和变量。extern中定义的内容都会被当做unsafe。
use std::os::raw::c_char;
extern {
fn strlen(s: *const c_char) -> usize;
}
use std::ffi::CString;
let rust_str = "I'll be back";
let null_terminated = CString::new(rust_str).unwrap();
unsafe {
assert_eq!(strlen(null_terminated.as_ptr()), 12);
}
CString::new(rust_str).unwrap()将一个Rust String转成CString。如果里面包含null会失败,所以返回的是Result。CString::new可以将任何实现了Into
CStr的as_ptr 方法返回*const c_char。CString可以deref成Cstr。
变量
extern可以定义全局变量,environ是POSIX系统里的一个变量,保存环境参数。
use std::ffi::CStr;
use std::os::raw::c_char;
extern {
static environ: *mut *mut c_char;
}
unsafe {
if !environ.is_null() && !(*environ).is_null() {
let var = CStr::from_ptr(*environ);
println!("first environment variable: {}",
var.to_string_lossy())
}
}
CStr::from_ptr从指针生成一个CStr指向这个字符串。to_string_lossy返回一个Cow�
代替。
外部库
放一个#[link(name = “git2”)]在extern上面,告诉编译器,下面的函数是从这个库来的。
动态连接
在编译的时候需要指定库的位置,可以通过床架build script的方法:再项目跟目录,也就是和Cargo.toml同级,放一个build.rs,这个代码只需要打印出需要的连接参数。
fn main() {
println!(r"cargo:rustc-link-search=native=/home/jimb/libgit2-0.25.1/build");
}
运行的时候也需要把依赖的库放到LD_LIBRARY_PATH (Linux)或者PATH(windows)
export LD_LIBRARY_PATH=/home/jimb/libgit2-0.25.1/build:$LD_LIBRARY_PATH
set PATH=C:\Users\JimB\libgit2-0.25.1\build\Debug;%PATH%
静态连接
也可以将依赖一起打包到rlib中。
rust的习惯是提供访问C库的crate一般被命名为LIB-sys,LIB是C库的名。一个LIB-sys库一般都是将C的库静态连接到库里。
额外的方法参考cargo文档
声明外部库的所有函数和变量是很麻烦的可以用bindgen,在build script里创建自动生成的方法
内存管理
从外部函数返回的指针指向外部函数内所创建和拥有的值。这些变量的生命周期由外部函数自己处理,而且它所指的内容很有可能被下一个调用所改变。
use std::ffi::CStr;
use std::os::raw::c_int;
fn check(activity: &'static str, status: c_int) -> c_int {
if status < 0 {
unsafe {
let error = &*raw::giterr_last();
println!("error while {}: {} ({})",
activity,
CStr::from_ptr(error.message).to_string_lossy(),
error.klass);
std::process::exit(1);
}
}
status
}
unsafe fn show_commit(commit: *const raw::git_commit) {
let author = raw::git_commit_author(commit);
let name = CStr::from_ptr((*author).name).to_string_lossy();
let email = CStr::from_ptr((*author).email).to_string_lossy();
println!("{} <{}>\n", name, email);
let message = raw::git_commit_message(commit);
println!("{}", CStr::from_ptr(message).to_string_lossy());
}
通过一个指针从外部函数获取的某个值,这个值的所有权是属于那个指针所指向的变量的。我们不用free它,但也不能持有这个获取的值超过原指针的生命周期,否则可能会变成悬垂指针。
也可以从外部函数获取所有权,git2通过其类型为指向指针的指针的第一个参数把所有权传递给调用者。
let mut repo = ptr::null_mut();
check("opening repository",
raw::git_repository_open(&mut repo, path.as_ptr()));
另外一种方式是在Rust先创建一个未初始化的值,但Rust不允许未初始化的值,所以用一个特殊的方法mem::MaybeUninit::uninit(),返回一个MaybeUninit
let oid = {
let mut oid = mem::MaybeUninit::uninit();
check("looking up HEAD",
raw::git_reference_name_to_id(oid.as_mut_ptr(), repo, c_name));
oid.assume_init()
};
实用工具
std::sync::Once可以用来做初始化, ONCE.call_once只可以被调用一次,执行过后,再有任何线程来调用都会直接返回。它是原子的,而且调用很cheap。
libc.atexit可以在结束时调用一个函数指针,c的函数指针和Rust的函数指针不同,但可以直接用定义在extern里的函数。因为panic跨越语言边界是UB,所以要保证atexit调用的函数不panic。POSIX 禁止在atexit再调用exit,但可以用std::process::abort直接放弃进程。
在使用phantomData的时候我们只用它来存生命周期参数,在函数调用的时候也可以使用一个参数来只用作限制生命周期。
/// Try to borrow a `&str` from `ptr`, given that `ptr` may be null or
/// refer to ill-formed UTF-8. Give the result a lifetime as if it were
/// borrowed from `_owner`.
///
/// Safety: if `ptr` is non-null, it must point to a null-terminated C
/// string that is safe to access for at least as long as the lifetime of
/// `_owner`.
unsafe fn char_ptr_to_str<T>(_owner: &T, ptr: *const c_char) -> Option<&str> {
if ptr.is_null() {
return None;
} else {
CStr::from_ptr(ptr).to_str().ok()
}
}
因为值是从raw pointer返回的,他是没有生命周期的,所以我们应该手动给他加上。_owner没有用到,但它的生命周期会隐式的赋给返回值。如果把上面的函数定义写完整是:
fn char_ptr_to_str<'o, T: 'o>(_owner: &'o T, ptr: *const c_char) -> Option<&'o str>