类型
基础类型
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>
