1. 入坑rust
1.1 rust发展历程
- 2006年,Mozilla 员工 “Graydon Hoare” 开发了Rust。
- 2015年5月15日,Rust编程语言核心团队正式宣布发布Rust 1.0版本,之后连续4年,在Stack Overflow开发者「最受喜爱编程语言」评选中获得第一名。
- 2019年7月4日,社交网络巨头Facebook联合其他100个行业巨头,对外宣布准备建设Libra(天秤座)项目。该项目的技术特点是:构建于区块链技术之上,并且基于Rust实现。
- 2019年7月18日,微软安全响应中心(MSRC)发文宣称:我们需要更安全的系统编程语言。
- 在suricata5.0上,rust已经成为一个必选组件,被用来编写应用层解析代码。
需要基于suricata开发项目的我,不得不入坑rust了。
1.2 rust安装
rustup 是rust官方的版本管理工具。应当作为安装 Rust 的首选。
在linux/mac下,运行一条命令就行了:
curl https://sh.rustup.rs -sSf | sh
但是由于众所周知的防火墙问题,我们需要配置一下中科大的源。
详细参考:https://zhuanlan.zhihu.com/p/26944087
2. Rust初印象
- rust对标c/c++,是一个编译型系统级编程语言(可以用来编写操作系统的那种)。
- Rust借用了C和C++的语法,它不允许空指针和悬挂指针,二者是C和C++中系统崩溃、内存泄露和不安全代码的根源。
- Rust做到了内存安全而无需.NET和Java编程语言中实现自动垃圾收集器的开销,这是通过所有权/借用机制、生命周期、以及类型系统来达到的。
- Rust 使用实现(implementation)、特征(trait)和结构化类型(structured type)而不是类(class)。这点,与基于继承的OO语言 C++, Java 有相当大的差异。
第一印象:
fn main() {
println!("Hello, world!");
}
复杂一点的猜猜看游戏
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
对于熟悉c/c++的我,读起来还是挺通顺的。
3. rust语言基础
3.1 变量和可变性
变量默认是不可改变的(immutable),当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
比如这样是不对的,rust编译就会出错。
我们可以在变量名之前加 mut 来使其可变。
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
变量还有一个“隐藏“的玩法,这样我们就可以像python那样,”改变“一个变量的类型了。(注意不是真正改变了变量类型,而是隐藏了之前的变量)
let spaces = " ";
let spaces = spaces.len();
3.2 数据类型
Rust是静态类型(staticallytyped)语言,也就是说在编译时就必须知道所有变量的类型。
比如,当我们想要将String转换为数字时,必须增加类型注解,
let guess = "42".parse().expect("Not a number!"); // 错误
let guess: u32 = "42".parse().expect("Not a number!"); // 正确
rust的原生数据类型(primitive types)包括:
// boolean type
let t = true;
let f: bool = false;
// char type
let c = 'c';
// numeric types
let x = 42;
let y: u32 = 123_456;
let z: f64 = 1.23e+2;
let zero = z.abs_sub(123.4);
let bin = 0b1111_0000;
let oct = 0o7320_1546;
let hex = 0xf23a_b049;
// string types
let str = "Hello, world!";
let mut string = str.to_string();
// arrays
let a = [0, 1, 2, 3, 4];
// tuples
let tuple: (i32, &str) = (50, "hello");
// raw pointers
let x = 5;
let raw = &x as *const i32;
// functions
fn plus_one(x: i32) -> i32 {
x + 1
}
// structure
# struct User {
# username: String,
# email: String,
# sign_in_count: u64,
# active: bool,
# }
#
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
3.3 控制流
rust的控制流和C/C++很类似。
条件:if/else if/else
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
循环:loop、while 和 for
loop
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
assert_eq!(result, 20);
}
while
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
}
for
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
4. 认识所有权
所有权(系统)是 Rust 最独特的功能,它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全。
所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!
4.1 作用域
{ // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
4.2 内存分配/释放
我们需要寻找一个存储在堆上的数据来探索 Rust 是如何知道该在何时清理数据的。
Rust的字符串类型,String是被分配到堆上的,能够存储在编译时未知大小的文本.
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{}", s); // 将打印 `hello, world!`
我们都知道,在c/c++中,分配到堆上的内存使用malloc/new,使用完需要释放free/delete。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。
{
let s = String::from("hello"); // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,
// s 不再有效
4.3 堆变量的移动
我们来看一个堆变量移动的例子:
let s1 = String::from("hello");
let s2 = s1;
我们将 s1 赋值给 s2,String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据
之前我们提到过当变量离开作用域后,Rust 自动调用 drop 函数并清理变量的堆内存。这就有了一个问题:当 s2 和 s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。
let s1 = String::from("hello");
let s2 = s1; // 这段代码执行之后,s1将不再能使用
println!("{}, world!", s1); // 错误
如下图所示,这种方式叫做变量“移动”。不是深拷贝也不是浅拷贝。
4.4 所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
4.5 返回值与作用域
返回值也可以转移所有权
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
4.6 引用
rust中的引用,类似c++中的应用,用&表示。
我们看一个例子
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
&s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
// 所以什么也不会发生
当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
我们将获取引用作为函数参数称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
如果我们尝试修改借用的变量呢?剧透:这行不通!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) { // & 表示引用(不可改变)
some_string.push_str(", world"); // 错误
}
我们通过一个小调整就能修复代码中的错误:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) { // &mut 就是可变引用啦
some_string.push_str(", world"); // 正确
}
4.7 slice 部分引用
字符串 slice(string slice)是 String 中一部分值的引用,它看起来像这样:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
5. 项目管理–cargo
cargo是rust的项目管理器,用过node.js的猿们,应该对node.js中的神器npm、grunt、gulp等工具印象深刻。作为新一代静态语言中的翘楚,rust官方参考了现有语言管理工具的优点,于是就产生了cargo。
言而总之,作为rust的代码组织管理工具,cargo提供了一系列的工具,从项目的建立、构建到测试、运行直至部署,为rust项目的管理提供尽可能完整的手段。
5.1 包(package)
当运行 cargo new 时是在创建一个包(package)
$ cargo new hello_world --bin
$ cd hello_world
$ tree .
.
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
项目编译
$ cargo build
项目运行
$ cargo run
Running `target/debug/hello_world`
Hello, world!
5.2 crate
Cargo 的约定是如果在代表包的 Cargo.toml 的同级目录下包含 src 目录且其中包含 main.rs 文件的话,Cargo 就知道这个包带有一个与包同名的二进制 crate,且 src/main.rs 就是 crate 根。
如果包(package)同时包含 src/main.rs 和 src/lib.rs,那么它带有两个 crate:一个库和一个二进制项目
5.3 模块(module)
随着工程的增大,把所有代码写在一个文件里面,是一件极其初等及愚蠢的作法。模块允许我们将代码组织起来,模块是几乎所有语言的基础设施,尽管叫法各有不同。
Rust 提供了一个关键字 mod,它可以在一个文件中定义一个模块,或者引用另外一个文件中的模块。
通常,我们会在单独的文件中写模块内容,然后使用 mod 关键字来加载那个文件作为我们的模块。
比如,我们在 src 下新建了文件 aaa.rs。现在目录结构是下面这样子:
foo
├── Cargo.toml
└── src
└── aaa.rs
└── main.rs
我们在 aaa.rs 中,写上:
pub fn print_aaa() {
println!("{}", 25);
}
在 main.rs 中,写上:
mod aaa;
use aaa::print_aaa;
fn main () {
print_aaa();
}
细心的朋友会发现,aaa.rs 中,没有使用 mod xxx {} 这样包裹起来,是因为 mod xxx; 相当于把 xxx.rs 文件用 mod xxx {} 包裹起来了。初学者往往会多加一层,请注意。
rust也支持层级的模块,这点和python很像。
详细参考:https://wiki.jikexueyuan.com/project/rust-primer/module/module.html
6. rust集合
就像C++的stl一样,Rust提供了一系列的基础且通用的容器类型。善用这些集合类型,可以让Rust编程更加方便轻松。
6.1 动态数组Vec
- 和我们之前接触到的Array不同,Vec具有动态的添加和删除元素的能力,并且能够以O(1)的效率进行随机访问。
- 同时,对其尾部进行push或者pop操作的效率也是平摊O(1)的。
- 同时,Vec的所有内容项都是生成在堆空间上的,也就是说,你可以轻易的将Vec move出一个栈而不用担心内存拷贝影响执行效率——毕竟只是拷贝的栈上的指针。
来段示例代码:
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
for i in &v {
println!("{}", i);
}
用过c++的朋友,是不是很熟悉
6.2 哈希 map
和动态数组Vec一样,哈希表(HashMap)也是Rust内置的集合类型之一,同属std::collections模块下。
它提供了一个平均复杂度为O(1)的查询方法,是实现快速搜索必备的类型之一。
来段代码:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name);
7. 错误处理
错误处理是保证程序健壮性的前提,在编程语言中错误处理的方式大致分为两种:抛出异常(exceptions)和作为值返回。
Rust 将错误作为值返回并且提供了原生的优雅的错误处理方案。
7.1 Result
熟练使用Result是编写 Rust 代码的关键,Rust 优雅的错误处理离不开值返回的错误形式,编写代码时提供给使用者详细的错误信息是值得推崇的。
enum Result<T, E> {
Ok(T),
Err(E),
}
T 和 E 是泛型类型参数;T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。
因为 Result 有这些泛型类型参数,我们可以将 Result 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
来一个打开文件出错的代码示例
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file: {:?}", error)
},
};
}
这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 f。match 之后,我们可以利用这个文件句柄来进行读写。
match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自 panic! 宏的输出:
thread 'main' panicked at 'There was a problem opening the file: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
来个更复杂的例子:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
},
other_error => panic!("There was a problem opening the file: {:?}", other_error),
},
};
}
7.2 panic
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。
panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。
Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。
在适当的场景使用 panic! 和 Result 将会使你的代码在面对不可避免的错误时显得更加可靠。
参考
https://www.rust-lang.org/zh-CN/learn
https://learnku.com/docs/rust-lang/2018
https://doc.rust-lang.org/stable/rust-by-example/
https://www.infoq.cn/article/TqYKkA-75wiA2cGauGdv
https://doc.rust-lang.org/cargo/index.html
来源:CSDN
作者:网络安全研发随想
链接:https://blog.csdn.net/gengzhikui1992/article/details/103990625