闭包

在函数中返回闭包:

  • 为了从函数返回一些东西,Rust 需要知道返回类型的大小,而不能直接返回一个trait,因此可以返回一个Box指针

  • 在函数中定义的变量放入闭包中返回,会因为移出作用域而失效,因此采用move关键字使得闭包获得它环境的所有权

1
2
3
4
5
6
7
8
9
10
11
fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;
Box::new(move |x| x + num)
}

fn main() {
let f = factory();

let answer = f(1);
assert_eq!(6, answer);
}

通用函数调用

不同的trait中有同名函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
trait Foo {
fn f(&self);
}

trait Bar {
fn f(&self);
}

struct Baz;

impl Foo for Baz {
fn f(&self) { println!("Baz’s impl of Foo"); }
}

impl Bar for Baz {
fn f(&self) { println!("Baz’s impl of Bar"); }
}

let b = Baz;
// b.f(); // 产生歧义,不能执行
Foo::f(&b);
Bar::f(&b);

结构体本身和trait中有同名函数,使用 <Type as Trait> 指明用的是哪个trait中的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
trait Foo {
fn foo() -> i32;
}

struct Bar;

impl Bar {
fn foo() -> i32 {
20
}
}

impl Foo for Bar {
fn foo() -> i32 {
10
}
}

fn main() {
assert_eq!(10, <Bar as Foo>::foo());
assert_eq!(20, Bar::foo());
}

包(crate)和模块(module)

基本调用:

  • 结构:

    1
    2
    3
    4
    5
    foo
    ├── Cargo.toml
    └── src
    └── aaa.rs
    └── main.rs
  • aaa.rs

    1
    2
    3
    pub fn print_aaa() {
    println!("{}", 25);
    }
  • main.rs

    1
    2
    3
    4
    5
    6
    7
    mod aaa;

    use self::aaa::print_aaa;

    fn main () {
    print_aaa();
    }

多层模块

  1. 优先查找 xxx.rs 文件
    1. main.rslib.rsmod.rs中的mod xxx : 默认优先查找同级目录下的 xxx.rs 文件;
    2. 其他文件yyy.rs中的mod xxx :默认优先查找同级目录的yyy目录下的 xxx.rs 文件;
  2. 如果 xxx.rs 不存在,则查找 xxx/mod.rs 文件,即 xxx 目录下的 mod.rs 文件。

例子

  • 结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    src
    ├── a
    │ ├── b
    │ │ ├── c
    │ │ │ ├── d.rs
    │ │ │ └── mod.rs
    │ │ └── mod.rs
    │ └── mod.rs
    └── main.rs
  • a/b/c/d.rs

    1
    2
    3
    pub fn print_ddd() {
    println!("i am ddd.");
    }
  • a/b/c/mod.rs:

    1
    pub mod d;
  • a/b/mod.rs:

    1
    pub mod c;
  • a/mod.rs:

    1
    pub mod b;
  • main.rs:

    1
    2
    3
    4
    5
    6
    7
    mod a;

    use self::a::b::c::d;

    fn main() {
    d::print_ddd();
    }

**注:**若在 a/mod.rs 中进行重导出(Re-exporting),即:

1
2
pub mod b;
pub use b::c::d;

则可以直接在 main.rs 中使用 use a::d; 来调用

路径

self 的含义:

  1. use self::xxx 表示,加载当前模块中的 xxx。此时 self 可省略;
  2. use xxx::{self, yyy},表示,加载当前路径下模块 xxx 本身,以及模块 xxx 下的 yyy

super 表示父模块:use super::xxx; 表示引用父模块中的 xxx

数组转基本类型

将包含4个u8的数组转换为u32类型

1
2
3
4
5
6
use std::mem;

unsafe {
let a = [0u8, 0u8, 0u8, 0u8];
let b = mem::transmute::<[u8; 4], u32>(a);
}

关联类型

将多个类型归为一类,如下,构建一个图(Graph),需要的类型包括点(Node)和边(Edge)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
trait Graph {
type N;
type E;

fn has_edge(&self, &Self::N, &Self::N) -> bool;
fn edges(&self, &Self::N) -> Vec<Self::E>;
}

struct Node;

struct Edge;

struct MyGraph;

impl Graph for MyGraph {
type N = Node;
type E = Edge;

fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
true
}

fn edges(&self, n: &Node) -> Vec<Edge> {
Vec::new()
}
}

let graph = MyGraph;
let obj = Box::new(graph) as Box<Graph<N=Node, E=Edge>>;

$开头的变量是语法元素,指示符用于限定愈发元素的类型,包括:

  • ident: 标识符,用来表示函数或变量名
  • expr: 表达式
  • block: 代码块,用花括号包起来的多个语句
  • pat: 模式,普通模式匹配(非宏本身的模式)中的模式,例如 Some(t), (3, 'a', _)
  • path: 路径,注意这里不是操作系统中的文件路径,而是用双冒号分隔的限定名(qualified name),如 std::cmp::PartialOrd
  • tt: 单个语法树
  • ty: 类型,语义层面的类型,如 i32, char
  • item: 条目,
  • meta: 元条目
  • stmt: 单条语句,如 let a = 42;

重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
macro_rules! vector {
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
$(temp_vec.push($x);)*
temp_vec
}
};
}

fn main() {
let a = vector![1, 2, 4, 8];
println!("{:?}", a);
}

递归

1
2
3
4
5
6
7
8
9
10
11
12
macro_rules! find_min {
($x:expr) => ($x);
($x:expr, $($y:expr),+) => (
std::cmp::min($x, find_min!($($y),+))
)
}

fn main() {
println!("{}", find_min!(1u32));
println!("{}", find_min!(1u32 + 2 , 2u32));
println!("{}", find_min!(5u32, 2u32 * 3, 4u32));
}

导入和导出

宏导入导出用 #[macro_use]#[macro_export]。父模块中定义的宏对其下的子模块是可见的,要想子模块中定义的宏在其后面的父模块中可用,需要使用 #[macro_use]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
macro_rules! m1 { () => (()) }

// 宏 m1 在这里可用

mod foo {
// 宏 m1 在这里可用

#[macro_export]
macro_rules! m2 { () => (()) }

// 宏 m1 和 m2 在这里可用
}

// 宏 m1 在这里可用
#[macro_export]
macro_rules! m3 { () => (()) }

// 宏 m1 和 m3 在这里可用

#[macro_use]
mod bar {
// 宏 m1 和 m3 在这里可用

macro_rules! m4 { () => (()) }

// 宏 m1, m3, m4 在这里均可用
}

// 宏 m1, m3, m4 均可用

crate 之间只有被标为 #[macro_export] 的宏可以被其它 crate 导入。假设上面例子是 foo crate 中的部分代码,则只有 m2m3 可以被其它 crate 导入。导入方式是在 extern crate foo; 前面加上 #[macro_use]

1
2
3
#[macro_use]
extern crate foo;
// foo 中 m2, m3 都被导入

如果只想导入 foo crate 中某个宏,比如 m3,就给 #[macro_use] 加上参数

1
2
3
#[macro_use(m3)]
extern crate foo;
// foo 中只有 m3 被导入

unsafe

  • 解引用一个裸指针*const T*mut T
1
2
3
4
let x = 5;
let raw = &x as *const i32;
let points_at = unsafe { *raw };
println!("raw points at {}", points_at);
  • 读写一个可变的静态变量static mut
1
2
3
4
5
static mut N: i32 = 5;
unsafe {
N += 1;
println!("N: {}", N);
}
  • 调用一个不安全函数
1
2
3
4
5
6
7
8
unsafe fn foo() {
//实现
}
fn main() {
unsafe {
foo();
}
}

迭代器

消费者

  • collect()

    1
    let one_to_one_hundred = (1..101).collect::<Vec<i32>>();
  • find()

    1
    let greater_than_forty_two = (0..100).find(|x| *x > 42);
  • fold()

    1
    2
    // 计算从迭代器的第0个元素开始,所有元素的累加
    let sum = (1..4).fold(0, |sum, x| sum + x);

迭代适配器

  • map

    1
    (1..100).map(|x| println!("{}", x));
  • take

    1
    2
    3
    for i in (1..).take(5) {
    println!("{}", i);
    }
  • filter

    1
    2
    3
    for i in (1..100).filter(|&x| x % 2 == 0) {
    println!("{}", i);
    }

FFI

调用ffi函数

引入libc库

Cargo.toml:

1
2
[dependencies]
libc = "0.2.86"

rs文件:

1
extern crate libc
声明ffi函数
1
2
3
4
5
6
7
8
9
10
use libc::c_int;
use libc::c_void;
use libc::size_t;

#[link(name = "yourlib")]
extern {
fn your_func(arg1: c_int, arg2: *mut c_void) -> size_t; // 声明ffi函数
fn your_func2(arg1: c_int, arg2: *mut c_void) -> size_t;
static ffi_global: c_int; // 声明ffi全局变量
}
调用ffi函数

由于此函数来自外部的c库,所以rust并不能保证该函数的安全性。因此,调用任何一个ffi函数需要一个unsafe块。

1
2
3
let result: size_t = unsafe {
your_func(1 as c_int, Box::into_raw(Box::new(3)) as *mut c_void)
};
封装unsafe,暴露安全接口

在一个叫ffi.rs之类的文件中写上所有的extern块用以声明ffi函数。在一个叫wrapper.rs之类的文件中进行包装:

1
2
3
4
5
// ffi.rs
#[link(name = "yourlib")]
extern {
fn your_func(arg1: c_int, arg2: *mut c_void) -> size_t;
}
1
2
3
4
5
// wrapper.rs
// 对外暴露(pub use)your_func_wrapper函数即可
fn your_func_wrapper(arg1: i32, arg2: &mut i32) -> isize {
unsafe { your_func(1 as c_int, Box::into_raw(Box::new(3)) as *mut c_void) } as isize
}

数据结构对应

结构体/Enum
1
2
3
4
5
6
7
8
9
#[repr(C)]
// 如果使用#[repr(C, packed)]将不为此结构体填充空位用以对齐
struct RustObject {
a: c_int,
// other members
}

#[repr(C)]
enum SomeEnum {}
回调函数

将一个rust函数转变成c可执行的回调函数非常简单:在函数前面加上extern "C":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern "C" fn callback(a: c_int) { // 这个函数是传给c调用的
println!("hello {}!", a);
}

#[link(name = "yourlib")]
extern {
fn run_callback(data: i32, cb: extern fn(i32));
}

fn main() {
unsafe {
// 调用c库中定义的run_callback函数,而run_callback函数中的回调函数callback则是在rust中定义的
run_callback(1 as i32, callback); // 打印 1
}
}

对应c库代码:

1
2
3
4
5
typedef void (*rust_callback)(int32_t);

void run_callback(int32_t data, rust_callback callback) {
callback(data); // 调用传过来的回调函数
}
字符串

CStr

对于产生于c的字符串(如在c程序中使用malloc产生),rust使用CStr来表示,和str类型对应,表明我们并不拥有这个字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::ffi::CStr;
use libc::c_char;

#[link(name = "yourlib")]
extern {
fn char_func() -> *mut c_char;
}

fn get_string() -> String {
unsafe {
let raw_string: *mut c_char = char_func();
let cstr = CStr::from_ptr(my_string());
cstr.to_string_lossy().into_owned()
}
}

在这里get_string使用CStr::from_ptr从c的char*获取一个字符串,并且转化成了一个String.

  • 注意to_string_lossy()的使用:因为在rust中一切字符都是采用utf8表示的而c不是, 因此如果要将c的字符串转换到rust字符串的话,需要检查是否都为有效utf-8字节。to_string_lossy将返回一个Cow<str>类型, 即如果c字符串都为有效utf-8字节,则将其0开销地转换成一个&str类型,若不是,rust会将其拷贝一份并且将非法字节用U+FFFD填充。

CString

CStr表示从c中来,rust不拥有归属权的字符串相反,CString表示由rust分配,用以传给c程序的字符串。

1
2
3
4
5
6
7
8
9
10
11
use std::ffi::CString;
use std::os::raw::c_char;

extern {
fn my_printer(s: *const c_char);
}

let c_to_print = CString::new("Hello, world!").unwrap();
unsafe {
my_printer(c_to_print.as_ptr()); // 使用 as_ptr 将CString转化成char指针传给c函数
}

注意c字符串中并不能包含\0字节(因为\0用来表示c字符串的结束符),因此CString::new将返回一个Result, 如果输入有\0的话则为Error(NulError)

空指针

另一种很常见的情况是需要一个空指针。请使用0 as *const _ 或者 std::ptr::null()来生产一个空指针。

将Rust编译成库

调用约定和mangle

由于rust支持重载,所以函数名会被编译器进行混淆,就像c++一样。因此当你的函数被编译完毕后,函数名会带上一串表明函数签名的字符串。

比如:fn test() {}会变成_ZN4test20hf06ae59e934e5641haaE. 这样的函数名为ffi调用带来了困难,因此,rust提供了#[no_mangle]属性为函数修饰。 对于带有#[no_mangle]属性的函数,rust编译器不会为它进行函数名混淆。

1
2
#[no_mangle]
extern "C" fn test() {}

使用Any

由于在跨越ffi过程中,rust类型信息会丢失,比如当用rust提供一个OpaqueStruct给别的语言时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::mem::transmute;

#[derive(Debug)]
struct Foo<T> {
t: T
}

#[no_mangle]
extern "C" fn new_foo_vec() -> *const c_void {
Box::into_raw(Box::new(Foo {t: vec![1,2,3]})) as *const c_void
}

#[no_mangle]
extern "C" fn new_foo_int() -> *const c_void {
Box::into_raw(Box::new(Foo {t: 1})) as *const c_void
}

fn push_foo_element(t: &mut Foo<Vec<i32>>) {
t.t.push(1);
}

#[no_mangle]
extern "C" fn push_foo_element_c(foo: *mut c_void){
let foo2 = unsafe {
&mut *(foo as *mut Foo<Vec<i32>>) // 这么确定是Foo<Vec<i32>>? 万一foo是Foo<i32>怎么办?
};
push_foo_element(foo3);
}

以上代码中完全不知道foo是一个什么东西。安全也无从说起了,只能靠文档。 因此在ffi调用时往往会丧失掉rust类型系统带来的方便和安全。在这里提供一个小技巧:使用Box<Box<Any>>来包装你的类型。

rustAny类型为rust带来了运行时反射的能力,使用Any跨越ffi边界将极大提高程序安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use std::any::Any;

#[derive(Debug)]
struct Foo<T> {
t: T
}

#[no_mangle]
extern "C" fn new_foo_vec() -> *const c_void {
Box::into_raw(Box::new(Box::new(Foo {t: vec![1,2,3]}) as Box<Any>)) as *const c_void
}

#[no_mangle]
extern "C" fn new_foo_int() -> *const c_void {
Box::into_raw(Box::new(Box::new(Foo {t: 1}) as Box<Any>)) as *const c_void
}

fn push_foo_element(t: &mut Foo<Vec<i32>>) {
t.t.push(1);
}

#[no_mangle]
extern "C" fn push_foo_element_c(foo: *mut c_void){
let foo2 = unsafe {
&mut *(foo as *mut Box<Any>)
};
let foo3: Option<&mut Foo<Vec<i32>>> = foo2.downcast_mut(); // 如果foo2不是*const Box<Foo<Vec<i32>>>, 则foo3将会是None
if let Some(value) = foo3 {
push_foo_element(value);
}
}

这样一来,就非常不容易出错了。

属性

属性只能应用于Item(元素、项), 例如 use 声明、模块、函数等。

在Rust中,Item是Crate(库)的一个组成部分。它包括

  • extern crate声明
  • use声明
  • 模块(模块是一个Item的容器)
  • 函数
  • type定义
  • 结构体定义
  • 枚举类型定义
  • 常量定义
  • 静态变量定义
  • Trait定义
  • 实现(Impl)

这些Item是可以互相嵌套的,比如在一个函数中定义一个静态变量、在一个模块中使用use声明或定义一个结构体。这些定义在某个作用域里面的Item跟你把 它写到最外层作用域所实现的功能是一样的,只不过你要访问这些嵌套的Item就必须使用路径(Path),如a::b::c。但一些外层的Item不允许你使用路径去访问它的子Item,比如函数,在函数中定义的静态变量、结构体等,是不可以通过路径来访问的。

属性语法

1
#[name(arg1, arg2 = "param")]

它是由一个#开启,后面紧接着一个[],里面便是属性的具体内容,它可以有如下几种写法:

  • 单个标识符代表的属性名,如#[unix]
  • 单个标识符代表属性名,后面紧跟着一个=,然后再跟着一个字面量(Literal),组成一个键值对,如#[link(name = "openssl")]
  • 单个标识符代表属性名,后面跟着一个逗号隔开的子属性的列表,如#[cfg(and(unix, not(windows)))]

#后面还可以紧跟一个!,比如#![feature(box_syntax)],这表示这个属性是应用于它所在的这个Item。而如果没有!则表示这个属性仅应用于紧接着的那个Item。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 为这个crate开启box_syntax这个新特性
#![feature(box_syntax)]

// 这是一个单元测试函数
#[test]
fn test_foo() {
/* ... */
}

// 条件编译,只会在编译目标为Linux时才会生效
#[cfg(target_os="linux")]
mod bar {
/* ... */
}

// 为以下的这个type定义关掉non_camel_case_types的编译警告
#[allow(non_camel_case_types)]
type int8_t = i8;

Crate的属性

  • crate_name - 指定Crate的名字。如#[crate_name = "my_crate"]则可以让编译出的库名字为libmy_crate.rlib

  • crate_type - 指定Crate的类型,有以下几种选择

    • "bin" - 编译为可执行文件;
    • "lib" - 编译为库;
    • "dylib" - 编译为动态链接库;
    • "staticlib" - 编译为静态链接库;
    • "rlib" - 编译为Rust特有的库文件,它是一种特殊的静态链接库格式,它里面会含有一些元数据供编译器使用,最终会静态链接到目标文件之中。

    #![crate_type = "dylib"]

  • feature - 可以开启一些不稳定特性,只可在nightly版的编译器中使用。

  • no_builtins - 去掉内建函数。

  • no_main- 不生成main这个符号,当你需要链接的库中已经定义了main函数时会用到。

  • no_start - 不链接自带的native库。

  • no_std - 不链接自带的std库。

  • plugin - 加载编译器插件,一般用于加载自定义的编译器插件库。用法是

    1
    2
    3
    4
    // 加载foo, bar两个插件
    #![plugin(foo, bar)]
    // 或者给插件传入必要的初始化参数
    #![plugin(foo(arg1, arg2))]
  • recursive_limit - 设置在编译期最大的递归层级。比如自动解引用、递归定义的宏等。默认设置是#![recursive_limit = "64"]

模块的属性

  • no_implicit_prelude - 取消自动插入use std::prelude::*

  • path - 设置此mod的文件路径。

    如声明mod a;,则寻找

    • 本文件夹下的a.rs文件
    • 本文件夹下的a/mod.rs文件
    1
    2
    3
    4
    5
    6
    7
    #[cfg(unix)]
    #[path = "sys/unix.rs"]
    mod sys;

    #[cfg(windows)]
    #[path = "sys/windows.rs"]
    mod sys;

函数的属性

  • main - 把这个函数作为入口函数,替代fn main,会被入口函数(Entry Point)调用。
  • plugin_registrar - 编写编译器插件时用,用于定义编译器插件的入口函数。
  • start - 把这个函数作为入口函数(Entry Point),改写 start language item。
  • test - 指明这个函数为单元测试函数,在非测试环境下不会被编译。
  • should_panic - 指明这个单元测试函数必然会panic。
  • cold - 指明这个函数很可能是不会被执行的,因此优化的时候特别对待它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 把`my_main`作为主函数
#[main]
fn my_main() {

}

// 把`plugin_registrar`作为此编译器插件的入口函数
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_macro("rn", expand_rn);
}

// 把`entry_point`作为入口函数,不再执行标准库中的初始化流程
#[start]
fn entry_point(argc: isize, argv: *const *const u8) -> isize {

}

// 定义一个单元测试
// 这个单元测试一定会panic
#[test]
#[should_panic]
fn my_test() {
panic!("I expected to be panicked");
}

// 这个函数很可能是不会执行的,
// 所以优化的时候就换种方式
#[cold]
fn unlikely_to_be_executed() {

}

全局静态变量的属性

  • thread_local - 只可用于static mut,表示这个变量是thread local的。

FFI的属性

extern块可以应用以下属性

  • link_args - 指定链接时给链接器的参数,平台和实现相关。

  • link - 说明这个块需要链接一个native库,它有以下参数:

    • name - 库的名字,比如libname.a的名字是name
    • kind - 库的类型,它包括
      • dylib - 动态链接库
      • static - 静态库
      • framework - OS X里的Framework
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #[link(name = "readline")]
    extern {

    }

    #[link(name = "CoreFoundation", kind = "framework")]
    extern {

    }

extern块里面,可以使用

对于enum类型,可以使用

  • repr - 目前接受CC表示兼容C ABI。
1
2
3
4
5
#[repr(C)]
enum eType {
Operator,
Indicator,
}

对于struct类型,可以使用

  • repr - 目前只接受CpackedC表示结构体兼容C ABI,packed表示移除字段间的padding。

宏的属性

  • macro_use - 把模块或库中定义的宏导出来

    • 应用于mod上,则把此模块内定义的宏导出到它的父模块中

    • 应用于extern crate上,则可以接受一个列表,如

      1
      2
      #[macro_use(debug, trace)]
      extern crate log;

      则可以只导入列表中指定的宏,若不指定则导入所有的宏。

  • macro_reexport - 应用于extern crate上,可以再把这些导入的宏再输出出去给别的库使用。

  • macro_export - 应于在宏上,可以使这个宏可以被导出给别的库使用。

  • no_link - 应用于extern crate上,表示即使我们把它里面的库导入进来了,但是不要把这个库链接到目标文件中。

其他属性

  • export_function - 用于静态变量或函数,指定它们在目标文件中的符号名。

  • link_section - 用于静态变量或函数,表示应该把它们放到哪个段中去。

  • no_mangle - 可以应用于任意的Item,表示取消对它们进行命名混淆,直接把它们的名字作为符号写到目标文件中。

  • simd - 可以用于元组结构体上,并自动实现了数值运算符,这些操作会生成相应的SIMD指令。

  • doc - 为这个Item绑定文档,跟///的功能一样,用法是

    1
    2
    #[doc = "This is a doc"]
    struct Foo {}

条件编译属性

有时候,我们想针对不同的编译目标来生成不同的代码,比如在编写跨平台模块时,针对Linux和Windows分别使用不同的代码逻辑。

条件编译基本上就是使用cfg这个属性,直接看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#[cfg(target_os = "macos")]
fn cross_platform() {
// Will only be compiled on Mac OS, including Mac OS X
}

#[cfg(target_os = "windows")]
fn cross_platform() {
// Will only be compiled on Windows
}

// 若条件`foo`或`bar`任意一个成立,则编译以下的Item
#[cfg(any(foo, bar))]
fn need_foo_or_bar() {

}

// 针对32位的Unix系统
#[cfg(all(unix, target_pointer_width = "32"))]
fn on_32bit_unix() {

}

// 若`foo`不成立时编译
#[cfg(not(foo))]
fn needs_not_foo() {

}

其中,cfg可接受的条件有

  • debug_assertions - 若没有开启编译优化时就会成立。
  • target_arch = "..." - 目标平台的CPU架构,包括但不限于x86, x86_64, mips, powerpc, armaarch64
  • target_endian = "..." - 目标平台的大小端,包括biglittle
  • target_env = "..." - 表示使用的运行库,比如musl表示使用的是MUSL的libc实现, msvc表示使用微软的MSVC,gnu表示使用GNU的实现。 但在部分平台这个数据是空的。
  • target_family = "..." - 表示目标操作系统的类别,比如windowsunix。这个属性可以直接作为条件使用,如#[unix]#[cfg(unix)]
  • target_os = "..." - 目标操作系统,包括但不限于windows, macos, ios, linux, android, freebsd, dragonfly, bitrig, openbsd, netbsd
  • target_pointer_width = "..." - 目标平台的指针宽度,一般就是3264
  • target_vendor = "..." - 生产商,例如apple, pc或大多数Linux系统的unknown
  • test - 当启动了单元测试时(即编译时加了--test参数,或使用cargo test)。

还可以根据一个条件去设置另一个条件,使用cfg_attr,如

1
#[cfg_attr(a, b)]

这表示若a成立,则这个就相当于#[cfg(b)]

条件编译属性只可以应用于Item,如果想应用在非Item中怎么办呢?可以使用cfg!宏,如

1
2
3
4
5
6
7
8
9
if cfg!(target_arch = "x86") {

} else if cfg!(target_arch = "x86_64") {

} else if cfg!(target_arch = "mips") {

} else {

}

这种方式不会产生任何运行时开销,因为不成立的条件相当于里面的代码根本不可能被执行,编译时会直接被优化掉。

内联参数

内联函数即建议编译器可以考虑把整个函数拷贝到调用者的函数体中,而不是生成一个call指令调用过去。这种优化对于短函数非常有用,有利于提高性能。

编译器自己会根据一些默认的条件来判断一个函数是不是应该内联,若一个不应该被内联的函数被内联了,实际上会导致整个程序更慢。

可选的属性有:

  • #[inline] - 建议编译器内联这个函数
  • #[inline(always)] - 要求编译器必须内联这个函数
  • #[inline(never)] - 要求编译器不要内联这个函数

内联会导致在一个库里面的代码被插入到另一个库中去。

自动实现Trait

编译器提供一个编译器插件叫作derive,它可以帮你去生成一些代码去实现(impl)一些特定的Trait,如

1
2
3
4
5
#[derive(PartialEq, Clone)]
struct Foo<T> {
a: i32,
b: T,
}

编译器会自动为你生成以下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
impl<T: PartialEq> PartialEq for Foo<T> {
fn eq(&self, other: &Foo<T>) -> bool {
self.a == other.a && self.b == other.b
}

fn ne(&self, other: &Foo<T>) -> bool {
self.a != other.a || self.b != other.b
}
}

impl<T: Clone> Clone for Foo<T> {
fn clone(&self) -> Foo<T> {
Foo {
a: self.a.clone(),
b: self.b.clone(),
}
}
}

目前derive仅支持标准库中部分的Trait。

编译器rustc参数

Rust编译器程序的名字是rustc,使用它的方法很简单:

1
$ rustc [OPTIONS] INPUT

其中,[OPTIONS]表示编译参数,而INPUT则表示输入文件。而编译参数有以下可选:

  • -h, --help - 输出帮助信息到标准输出;

  • --cfg SPEC - 传入自定义的条件编译参数,使用方法如

    1
    2
    3
    4
    5
    fn main() {
    if cfg!(hello) {
    println!("world!");
    }
    }

    如上例所示,若cfg!(hello)成立,则运行程序就会输出"world"到标准输出。我们把这个文件保存为hello.rs然后编译它

    1
    $ rustc --cfg hello hello.rs

    运行它就会看到屏幕中输出了world!

  • -L [KIND=]PATH - 往链接路径中加入一个文件夹,并且可以指定这个路径的类型(Kind),这些类型包括

    • dependency - 在这个路径下找依赖的文件,比如说mod
    • crate - 只在这个路径下找extern crate中定义的库;
    • native - 只在这个路径下找Native库;
    • framework - 只在OS X下有用,只在这个路径下找Framework;
    • all - 默认选项。
  • -l [KIND=]NAME - 链接一个库,这个库可以指定类型(Kind)

    • static - 静态库;
    • dylib - 动态库;
    • framework - OS X的Framework。

    如果不传,默认为dylib

    此处举一个例子如何手动链接一个库,我们先创建一个文件叫myhello.rs,在里面写一个函数

    1
    2
    3
    4
    5
    6
    7
    // myhello.rs

    /// 这个函数仅仅向标签输出打印 Hello World!
    /// 不要忘记要把它标记为 pub 哦。
    pub fn print_hello() {
    println!("Hello World!");
    }

    然后把这个文件编译成一个静态库,libmyhello.a

    1
    $ rustc --crate-type staticlib myhello.rs

    然后再创建一个main.rs,链接这个库并打印出"Hello World!"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // main.rs

    // 指定链接库 myhello
    extern crate myhello;

    fn main() {
    // 调用库函数
    myhello::print_hello();
    }

    编译main.rs

    1
    $ rustc -L. -lmyhello main.rs

    运行main,就会看到屏幕输出"Hello World!"啦。

  • --crate-type - 指定编译输出类型,它的参数包括

    • bin - 二进行可执行文件
    • lib - 编译为库
    • rlib - Rust库
    • dylib - 动态链接库
    • staticlib - 静态链接库
  • --crate-name - 指定这个Crate的名字,默认是文件名,如main.rs编译成可执行文件时默认是main,但你可以指定它为foo

    1
    $ rustc --crate-name foo main.rs

    则会输出foo可执行文件。

  • --emit - 指定编译器的输出。编译器默认是输出一个可执行文件或库文件,但你可以选择输出一些其它的东西用于Debug

    • asm - 输出汇编
    • llvm-bc - LLVM Bitcode
    • llvm-ir - LLVM IR,即LLVM中间码(LLVM Intermediate Representation);
    • obj - Object File(就是*.o文件);
    • link - 这个是要结合其它--emit参数使用,会执行Linker再输出结果;
    • dep-info - 文件依赖关系(Debug用,类似于Makefile一样的依赖)。

    以上参数可以同时使用,使用逗号分割,如

    1
    $ rustc --emit asm,llvm-ir,obj main.rs

    同时,在最后可以加一个=PATH来指定输出到一个特定文件,如

    1
    $ rustc --emit asm=output.S,llvm-ir=output.ir main.rs

    这样会把汇编生成到output.S文件中,把LLVM中间码输出到output.ir中。

  • --print - 打印一些信息,参数有

    • crate-name - 编译目标名;
    • file-names - 编译的文件名;
    • sysroot - 打印Rust工具链的根目录地址。
  • -g - 在目标文件中保存符号,这个参数等同于-C debuginfo=2

  • -O - 开启优化,这个参数等同于-C opt-level=2

  • -o FILENAME - 指定输出文件名,同样适用于--emit的输出。

  • --out-dir DIR - 指定输出的文件夹,默认是当前文件夹,且会忽略-o配置。

  • --explain OPT - 解释某一个编译错误,比如

    若你写了一个main.rs,使用了一个未定义变量f

    1
    2
    3
    fn main() {
    f
    }

    编译它时编译器会报错:

    1
    2
    3
    4
    5
    main.rs:2:5: 2:6 error: unresolved name `f` [E0425]
    main.rs:2 f
    ^
    main.rs:2:5: 2:6 help: run `rustc --explain E0425` to see a detailed explanation
    error: aborting due to previous error

    虽然错误已经很明显,但是你也可以让编译器解释一下,什么是E0425错误:

    1
    2
    $ rustc --explain E0425
    // 编译器打印的说明
  • --test - 编译成一个单元测试可执行文件

  • --target TRIPLE - 指定目标平台,基本格式是cpu-manufacturer-kernel[-os],例如

    1
    2
    ## 64位OS X
    $ rustc --target x86_64-apple-darwin
  • -W help - 打印Linter的所有可配置选项和默认值。

  • -W OPT, --warn OPT - 设置某一个Linter选项为Warning。

  • -A OPT, --allow OPT - 设置某一个Linter选项为Allow。

  • -D OPT, --deny OPT - 设置某一个Linter选项为Deny。

  • -F OPT, --forbit OPT - 设置某一个Linter选项为Forbit。

  • -C FLAG[=VAL], --codegen FLAG[=VAL] - 目标代码生成的的相关参数,可以用-C help来查看配置,值得关注的几个是

    • linker=val - 指定链接器;
    • linker-args=val - 指定链接器的参数;
    • prefer-dynamic - 默认Rust编译是静态链接,选择这个配置将改为动态链接;
    • debug-info=level - Debug信息级数,0 = 不生成,1 = 只生成文件行号表,2 = 全部生成;
    • opt-level=val - 优化级数,可选0-3
    • debug_assertion - 显式开启cfg(debug_assertion)条件。
  • -V, --version - 打印编译器版本号。

  • -v, --verbose - 开启啰嗦模式(打印编译器执行的日志)。

  • --extern NAME=PATH - 用来指定外部的Rust库(*.rlib)名字和路径,名字应该与extern crate中指定的一样。

  • --sysroot PATH - 指定工具链根目录。

  • -Z flag - 编译器Debug用的参数,可以用-Z help来查看可用参数。

  • --color auto|always|never - 输出时对日志加颜色

    • auto - 自动选择加还是不加,如果输出目标是虚拟终端(TTY)的话就加,否则就不加;
    • always - 给我加!
    • never - 你敢加?

Cargo.toml参数配置

[package]段落

啥也不多说了,直接上例子,大家注意我在例子中的中文解释,个人觉得这样比较一目了然:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[package]
# 软件包名称,如果需要在别的地方引用此软件包,请用此名称。
name = "hello_world"

# 当前版本号,这里遵循semver标准,也就是语义化版本控制标准。
version = "0.1.0" # the current version, obeying semver

# 软件所有作者列表
authors = ["you@example.com"]

# 非常有用的一个字段,如果要自定义自己的构建工作流,
# 尤其是要调用外部工具来构建其他本地语言(C、C++、D等)开发的软件包时。
# 这时,自定义的构建流程可以使用rust语言,写在"build.rs"文件中。
build = "build.rs"

# 显式声明软件包文件夹内哪些文件被排除在项目的构建流程之外,
# 哪些文件包含在项目的构建流程中
exclude = ["build/**/*.o", "doc/**/*.html"]
include = ["src/**/*", "Cargo.toml"]

# 当软件包在向公共仓库发布时出现错误时,使能此字段可以阻止此错误。
publish = false

# 关于软件包的一个简短介绍。
description = "..."

# 下面这些字段标明了软件包仓库的更多信息
documentation = "..."
homepage = "..."
repository = "..."

# 顾名思义,此字段指向的文件就是传说中的ReadMe,
# 并且,此文件的内容最终会保存在注册表数据库中。
readme = "..."

# 用于分类和检索的关键词。
keywords = ["...", "..."]

# 软件包的许可证,必须是cargo仓库已列出的已知的标准许可证。
license = "..."

# 软件包的非标许可证书对应的文件路径。
license-file = "..."

依赖的详细配置

最直接的方式:

1
2
3
[dependencies]
hammer = "0.5.0"
color = "> 0.6.0, < 0.8.0"

与平台相关的依赖定义格式不变,不同的是需要定义在[target]字段下。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 注意,此处的cfg可以使用not、any、all等操作符任意组合键值对。
# 并且此用法仅支持cargo 0.9.0(rust 1.8.0)以上版本。
# 如果是windows平台,则需要此依赖。
[target.'cfg(windows)'.dependencies]
winhttp = "0.4.0"

[target.'cfg(unix)'.dependencies]
openssl = "1.0.1"

#如果是32位平台,则需要此依赖。
[target.'cfg(target_pointer_width = "32")'.dependencies]
native = { path = "native/i686" }

[target.'cfg(target_pointer_width = "64")'.dependencies]
native = { path = "native/i686" }

# 另一种写法就是列出平台的全称描述
[target.x86_64-pc-windows-gnu.dependencies]
winhttp = "0.4.0"
[target.i686-unknown-linux-gnu.dependencies]
openssl = "1.0.1"

# 如果使用自定义平台,请将自定义平台文件的完整路径用双引号包含
[target."x86_64/windows.json".dependencies]
winhttp = "0.4.0"
[target."i686/linux.json".dependencies]
openssl = "1.0.1"
native = { path = "native/i686" }
openssl = "1.0.1"
native = { path = "native/x86_64" }

# [dev-dependencies]段落的格式等同于[dependencies]段落,
# 不同之处在于,[dependencies]段落声明的依赖用于构建软件包,
# 而[dev-dependencies]段落声明的依赖仅用于构建测试和性能评估。
# 此外,[dev-dependencies]段落声明的依赖不会传递给其他依赖本软件包的项目
[dev-dependencies]
iron = "0.2"

自定义编译器调用方式模板详细参数

cargo内置五种编译器调用模板,分别为dev、release、test、bench、doc,分别用于定义不同类型生成目标时的编译器参数,如果我们自己想改变这些编译模板,可以自己定义相应字段的值,例如(注意:下述例子中列出的值均为此模板字段对应的系统默认值):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 开发模板, 对应`cargo build`命令
[profile.dev]
opt-level = 0 # 控制编译器的 --opt-level 参数,也就是优化参数
debug = true # 控制编译器是否开启 `-g` 参数
rpath = false # 控制编译器的 `-C rpath` 参数
lto = false # 控制`-C lto` 参数,此参数影响可执行文件和静态库的生成,
debug-assertions = true # 控制调试断言是否开启
codegen-units = 1 # 控制编译器的 `-C codegen-units` 参数。注意,当`lto = true`时,此字段值被忽略

# 发布模板, 对应`cargo build --release`命令
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = false
debug-assertions = false
codegen-units = 1

# 测试模板,对应`cargo test`命令
[profile.test]
opt-level = 0
debug = true
rpath = false
lto = false
debug-assertions = true
codegen-units = 1

# 性能评估模板,对应`cargo bench`命令
[profile.bench]
opt-level = 3
debug = false
rpath = false
lto = false
debug-assertions = false
codegen-units = 1

# 文档模板,对应`cargo doc`命令
[profile.doc]
opt-level = 0
debug = true
rpath = false
lto = false
debug-assertions = true
codegen-units = 1

需要注意的是,当调用编译器时,只有位于调用最顶层的软件包的模板文件有效,其他的子软件包或者依赖软件包的模板定义将被顶层软件包的模板覆盖。

[features]段落

[features]段落中的字段被用于条件编译选项或者是可选依赖。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[package]
name = "awesome"

[features]
# 此字段设置了可选依赖的默认选择列表,
# 注意这里的"session"并非一个软件包名称,
# 而是另一个featrue字段session
default = ["jquery", "uglifier", "session"]

# 类似这样的值为空的feature一般用于条件编译,
# 类似于`#[cfg(feature = "go-faster")]`。
go-faster = []

# 此feature依赖于bcrypt软件包,
# 这样封装的好处是未来可以对secure-password此feature增加可选项目。
secure-password = ["bcrypt"]

# 此处的session字段导入了cookie软件包中的feature段落中的session字段
session = ["cookie/session"]

[dependencies]
# 必要的依赖
cookie = "1.2.0"
oauth = "1.1.0"
route-recognizer = "=2.1.0"

# 可选依赖
jquery = { version = "1.0.2", optional = true }
uglifier = { version = "1.5.3", optional = true }
bcrypt = { version = "*", optional = true }
civet = { version = "*", optional = true }

如果其他软件包要依赖使用上述awesome软件包,可以在其描述文件中这样写:

1
2
3
4
[dependencies.awesome]
version = "1.3.5"
default-features = false # 禁用awesome 的默认features
features = ["secure-password", "civet"] # 使用此处列举的各项features

使用features时需要遵循以下规则:

  • feature名称在本描述文件中不能与出现的软件包名称冲突
  • 除了default feature,其他所有的features均是可选的
  • features不能相互循环包含
  • 开发依赖包不能包含在内
  • features组只能依赖于可选软件包

features的一个重要用途就是,当开发者需要对软件包进行最终的发布时,在进行构建时可以声明暴露给终端用户的features,这可以通过下述命令实现:

1
$ cargo build --release --features "shumway pdf"

关于测试

当运行cargo test命令时,cargo将会按做以下事情:

  • 编译并运行软件包源代码中被#[cfg(test)] 所标志的单元测试
  • 编译并运行文档测试
  • 编译并运行集成测试
  • 编译examples

配置构建目标

所有的诸如[[bin]], [lib], [[bench]], [[test]]以及 [[example]]等字段,均提供了类似的配置,以说明构建目标应该怎样被构建。例如(下述例子中[lib]段落中各字段值均为默认值):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[lib]
# 库名称,默认与项目名称相同
name = "foo"

# 此选项仅用于[lib]段落,其决定构建目标的构建方式,
# 可以取dylib, rlib, staticlib 三种值之一,表示生成动态库、r库或者静态库。
crate-type = ["dylib"]

# path字段声明了此构建目标相对于cargo.toml文件的相对路径
path = "src/lib.rs"

# 单元测试开关选项
test = true

# 文档测试开关选项
doctest = true

# 性能评估开关选项
bench = true

# 文档生成开关选项
doc = true

# 是否构建为编译器插件的开关选项
plugin = false

# 如果设置为false,`cargo test`将会忽略传递给rustc的--test参数。
harness = true

no_std

OS提供系统调用来支持IO、网络、文件系统等的操作,Rust利用OS的特性来实现如发送消息到控制台、读取文件、打开URL等标准库(std)的操作。

若OS不提供这些系统调用,则这种特性被称为no_std,对于Rust的no_std我们只能使用不依赖OS的特性,如core crate

no_std与std的区别在于:

  • no_std下,不能使用std crate,但能使用大部分core crate中的模块
  • 不能使用与堆相关的模块(box, collections, string等),因为Rust内存分配机制默认依赖于OS系统调用实现堆的分配
  • 如果写一个二进制crate,则必须实现一些lang items(rustc允许用户通过lang item来定制语言特性,而不是将所有操作都嵌入到编译器中)

https://justjjy.com/Rust-no-std

外部crate

lazy_static

给静态变量延迟赋值的宏。

使用这个宏,所有 static类型的变量可在执行的代码在运行时被初始化。 这包括任何需要堆分配,如vectorhash map,以及任何非常量函数调用。

由于conststatic在初始化时,必须赋予一个常量表达式的值,如果想要初始化动态的数组,vector,map,结果是编译不通过:

1
2
static VEC:Vec<u8> = vec![0x18u8, 0x11u8];
static MAP: HashMap<u32, String> = HashMap::new();

使用 lazy_static 消除上面所有问题,在使用时需要解引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[macro_use]
extern crate lazy_static;

use std::collections::HashMap;

lazy_static! {
static ref VEC:Vec<u8> = vec![0x18u8, 0x11u8];
static ref MAP: HashMap<u32, String> = {
let mut map = HashMap::new();
map.insert(18, "hury".to_owned());
map
};
static ref PAGE:u32 = mulit(18);
}

fn mulit(i: u32) -> u32 {
i * 2
}

fn main() {
println!("{:?}", *PAGE);
println!("{:?}", *VEC);
println!("{:?}", *MAP);
}

创建全局静态可变变量

1
2
3
4
5
6
7
8
use std::{collections::HashMap, sync::Mutex};

#[macro_use]
extern crate lazy_static;

lazy_static!{
static ref ENCLAVE_ID_MAP: Mutex<HashMap<sgx_enclave_id_t, u32>> = Mutex::new(HashMap::new());
}

使用:

1
2
3
4
ENCLAVE_ID_MAP.lock().unwrap().insert(eid1, 1);
ENCLAVE_ID_MAP.lock().unwrap().insert(eid2, 2);
ENCLAVE_ID_MAP.lock().unwrap().insert(eid3, 3);
println!("{:?}", ENCLAVE_ID_MAP.lock().unwrap());