Rust入门

rust 入门

官网

Rust 程序设计语言 (rust-lang.org)

安装

  1. 官网下载Rustup执行
  2. 安装Visual Studio,勾选C++桌面开发选项(window环境)

helloworld

创建项目

cargo new hello

编译

cargo build

执行

target/debug目录下

hello.exe

类型系统概述

什么是类型?

类型是对二进制数据的一种约束行为.类型比起直接使用二进制数据,有许多优势:

  • 少开发者心智负担
  • 安全
  • 容易优化

常见的类型分类

  • 态类型:在编译期对类型进行检查
  • 动态类型:在运行期对类型进行检查
  • 强类型:不允许隐式类型转换
  • 弱类型:允许进行隐式类型转换

C语言由于允许隐式类型转换因此是静态弱类型语言,许多人易将C语言误认为静态强类型,需要特别注意

c 复制代码
int main() {
	long a  = 10;
    return a;
}

Rust是静态强类型语言

变量和可变性

创建和使用变量

在Rust代码中,可以使用 let关键字将值绑定到变量

rust 复制代码
fu main() {
    let x = 5;
  
}

println是一个宏,它是最常用的将数字打印在屏幕上的方法.目前,我们可以简单地将它视为一个拥有可变参数数量的函数,在后面的章节中我们会对宏进行详细的讨论

可变性

在Rust中,变量默认是不可变的,一旦一个值绑定到一个名称,就不能更改该值

rust 复制代码
fn main() {
    let x = 5;
    println!("The value of x is:{}", x);
    x = 6; //can not assign twice to immutable variable `x`
    println!("The value of x is:{}", x);
}

但有时候允许变量可变是非常有用的.通过在变量名前面添加 mut来使它们可变

rust 复制代码
fn main() {
    let mut x = 5;
    println!("The value of x is:{}", x);
    x = 6;
    println!("The value of x is:{}", x);
}

常量和变量不可变变量

容易让你联想到另一个概念:常量。在Rust中,常量使用 const定义,而变量使用 let定义。

  • 不允许对常量使用修饰词 mut,常量始终是不可变的
  • 必须显示标注常量的类型
  • 常量可以在任何作用域中声明,包括全局作用域
  • 常量只能设置为常量表达式,而不能设置为函数调用的结果或只能在运行时计算的任何其他值
rust 复制代码
const A_CONST: i32 = 1;

隐藏

可以声明一个与前一个变量同名的新变量,并且新变量会隐藏前一个变量,这种操作被成为隐藏(Shadowing)

rust 复制代码
fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;
    println!("The value of x is: {}", x);
}

基础数据类型

Rust是一门静态编程语言,所有变量的类型必须在编译期就被明确固定

整数

Rust中有12种不同的整数类型

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize
  • 对于未明确标注类型的整数,Rust默认采用i32
  • isize和usize根据系统的不同而有不同的长度

浮点数

Rust有两种浮点数类型,为 f32f64,后者精度更高

对于未明确标注类型的小数,Rust默认采用f64

rust 复制代码
fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0; // f32
}

布尔值

与大多数其他编程语言一样,Rust中的布尔类型有两个可能的值:true和false

布尔值的大小是一个字节

rust 复制代码
fn main() {
    let t = true;
    let f: bool = false;
}

字符

Rust支持单个字符,字符使用单引号包装

rust 复制代码
fn main() {
    let c = 'z';
    let p='🐷';
}

整数溢出

rust 复制代码
fn avg(a: u32, b: u32) -> u32 {
    // (a + b) / 2
    (a & b) + ((a ^ b) >> 1)
}

fn main() {
    assert_eq!(avg(4294967295, 4294967295), 4294967295);
    assert_eq!(avg(0, 0), 0);
    assert_eq!(avg(10, 20), 15);
    assert_eq!(avg(4294967295, 1), 2147483648);
    println!("passed");
}

在电脑领域里所发生的溢出条件是,运行单项数值计算时,当计算产生出来的结果是非常大的,大于寄存器或存储器所能存储或表示的能力限制就会发生溢出

在不同的编程语言中,对待溢出通常有以下几种不同的做法

  • 崩溃:当溢出被侦测到时,程序立即退出运行
  • 忽略:这是最普遍的作法,忽略任何算数溢出

对于溢出的处理方法,Rust在debug与release模式下是不同的.在debug模式下编译时,Rust会检查整数溢出,如果发生这种行为,会导致程序在运行时终止并报出运行时错误。而如果在release模式下编译时,Rust不会对整数溢出进行检查

要显式处理溢出,可以使用标准库提供的一些 .overflowing_*方法:

rust 复制代码
fn main() {
    // let a: u32 = "4294967295".parse::<u32>().unwrap();
    let a: u32 = 4294967295;
    let b: u32 = 1;

    let (r, is_overflow) = a.overflowing_add(b);
    println!("r={} is_overflow={}", r, is_overflow)
}

元组

元组是将多个具有各种类型的值组合成一个复合类型的通用方法。元组有固定的长度:一旦声明,它们的大小就不能增长或收缩

我们通过在括号内写一个逗号分隔的值列表来创建一个元组。元组中的每个位置都有一个类型,元组中不同值的类型不必相同

rust 复制代码
fn main() {
    let a: i32 = 10;
    let b: char = 'A'; //创建一个元组
    let my_tuple: (i32, char) = (a, b); //从元组中读取一个值
    println!(".0={:?}", my_tuple.0);
    println!(".1={:?}", my_tuple.1);

    //解封装
    let (c, d) = my_tuple;
    println!("c={} d={}", c, d);
}

数组

另一种拥有多个数据集合的方法是使用数组与元组不同,数组中的每个元素都必须具有相同的类型Rust中的数组不同于其他一些语言中的数组,Rust中的数组具有固定长度

数组下标以0开始,同时Rust存在越界检查

rust 复制代码
fn main() {
    // 创建数组,[i32; 3]是数组的类型提示,表示元素的类型是i32,共有3个元素
    let myarray: [i32; 3] = [1, 2, 3];
    // 根据索引获取一个值,数组下标从0开始
    println!("{:?}", myarray[1]);

    // 索引不能越界
    println!("{:?}", myarray[3]);

    // 如果数组的每个元素都有相同的值,我们还可以简化数组的初始化
    let myarray: [i32; 3] = [0; 3];
    println!("{:?}", myarray[1]);
}

切片类型

切片类型是对一个数组(包括固定大小数组和动态数组)的引|用片段,有利于安全有效地访问数组的一部分,而不需要拷贝数组或数组中的内容。切片在编译的时候其长度是未知的,在底层实现上,一个切片保存着两个uszie成员,第一个usize成员指向切片起始位置的指针,第二个usize成员表示切片长度

rust 复制代码
fn main() {
    let mut arr: [i32; 5] = [1, 2, 3, 4, 5];
    let slice = &arr[0..3]; // ..是Rust Range语法 & 是引用符号

    println!("slice[0]={}, len={}", slice[0], slice.len());

    let slice2 = &arr[3..5];
    println!("slice2[0]={} slice2[1]={}", slice2[0], slice2[1]);
    println!("slice2_len={}", slice2.len());

    let mut slice3 = &mut arr[..];
    slice3[0] = 6;
    println!("arr[θ]={}", arr[0]);
}

结构体

结构体是多种不同数据类型的组合。它与元组类似,但区别在于我们可以为每个成员命名。可以使用struct关键字创建三种类型的结构:

  • 元组结构
  • 经典的C结构
  • 无字段的单元结构

结构体使用驼峰命名

rust 复制代码
//元组结构
struct Pair(i32, f32);

//经典的C结构
#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
}

//无字段的单元结构,在泛型中较为常用
struct Unit;

fn main() {
    //结构体的实例化
    let pair = Pair(10, 4.2);
    let person = Person {
        name: String::from("jack"),
        age: 21,
    };
    let unit = Unit;

    //从结构体中获取成员
    println!("{}", pair.0);
    println!("name={}, age={}", person.name, person.age);
    println!("{:?}", person);
}

枚举

enum关键字可创建枚举类型.枚举类型包含了取值的全部可能的情况.在Rust中,有多种不同形式的枚举写法

无参数的枚举
rust 复制代码
enum Planet {
    Mars,
    Earth,
}

上面的代码定义了枚举Planet,包含了两个值Mars和Earth

带枚举值的枚举
rust 复制代码
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}
带参数的枚举

Rust还支持携带类型参数的枚举

rust 复制代码
enum IpAddr {
    IPv4(u8, u8, u8, u8),
    IPv6(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8),
}
模式匹配

枚举通常与match模式匹配一起使用

rust 复制代码
enum IpAddr {
    IPv4(u8, u8, u8, u8),
    IPv6(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8),
}

fn main() {
    let localhost: IpAddr = IpAddr::IPv4(127, 0, 0, 1);
    match localhost {
        IpAddr::IPv4(a, b, c, d) => {
            println!("{} {} {} {}", a, b, c, d);
        }
        _ => {} // 任何非 IPv4 类型的走这条分支
    }
}

在不同类型之间转换

Rust是一门强类型语,因此不支持隐式类型转换。Rust为了实现类型之间的转换提供了几种不同的方法

as 语法

as语法是Rust最基础的一种类型转换方法,它通常用于整数,浮点数和字符数据之间的类型转换

rust 复制代码
fn main() {
    let a: i8 = -10;
    let b = a as u8;
    println!("a={} b={}", a, b);
}

数值转换的语义是:

  • 两个相同大小的整型之间(例如:i32->u32)的转换是一个 no-op
  • 从一个大的整型转换为一个小的整型(例如:u32->u8)会截断
  • 从一个小的整型转换为一个大的整型(例如:u8->u32)会
    • 如果源类型是无符号的会补零(zero-extend)
    • 如果源类型是有符号的会符号(sign-extend)
  • 从一个浮点转换为一个整型会向0舍入
  • 从一个整型转换为一个浮点会产生整型的浮点表示,如有必要会舍入(未指定舍入策略)
  • 从f32转换为f64是完美无缺的
  • 从f64转换为f32会产生最接近的可能值(未指定舍入策略)
transmute

as 只允许安全的转换,例如会拒绝例如尝试将4个字节转换为一个u32:

rust 复制代码
fn main() {
    let a = [0u8, 0u8, 0u8, 0u8];
    let b = a as u32; // Four u8s makes a u32.
}

但是我们知道u32在内存中表示为4个连续的u8,因此我们可以使用一种危险的方法:告诉编译器直接以另一种数据类型对待内存中的数据.编译器会无条件信任你。但是,除非你知道自己在干什么,不然并不推荐使用transmute。要使用transmute,需要将代码写入 unsafe块中。

rust 复制代码
use core::mem;

fn main() {
    unsafe {
        // 字节序
        // 大端序 小端序,rust使用小端序
        //              00000000    10000000    00000000    00000000
        let a = [0u8,       1u8,        0u8,        0u8];
        // b = 0b1_00000000
        let b = mem::transmute::<[u8; 4], u32>(a);
        println!("{}", b); // 256
        //Or, more concisely:
        let c: u32 = mem::transmute(a);
        println!("{}", c); // 256
    }
}

Rust 流程控制

表达式的多种形式

语句?表达式?

语句在英文中是statement,表达式则是expression.我们可能常常听说过"赋值语句"或者"算数表达式"这些名词,但是你有想过为什么不是"赋值表达式"吗?语句和表达式有一个重要的区别在于,表达式总是返回一个值,而语句不会.例如:

rust 复制代码
1 + 1;	// 这是表达式
let a = 1;	// 这是语句

Rust是一个基于表达式的语言,这意味着它的大多数代码都有一个返回值.除了以下几种语法:

  • 变量声明
  • 模块声明
  • 函数声明
  • 结构体声明
  • 枚举声明
  • ...

你可能会奇怪为什么if...else...不在上面的列表中,事实上,在Rust中,条件与循环并不是语句,而是表达式,这意味着它可以有返回值!这可能是你首先会疑惑的地方---这看起来和C不太一样!

if表达式,实现类似C语言中的三值表达式的功能

rust 复制代码
let cond = true;
    let a = if cond {
        42
    } else {
        24
    };

loop表达式的break语句后可跟着一个返回值返回

rust 复制代码
    let mut s = 0;
    let mut n = 10;
    let a = loop {
        if n < 0 {
            break s;
        }
        s += n;
        n -= 1;
    };
    println!("{:?}", a)

if-else

Rust中的if-else语法与其他语言类似.与许多语言不同,if后的布尔条件不需要用括号括起来

  • 如果使用if-else返回一个值,则所有分支必须返回相同的类型
rust 复制代码
fn main() {
    let n = 5;
    if n < 0 {
        print!("{} is negative", n);
    } else if n > 0 {
        print!("{} is positive", n);
    } else {
        print!("{} is zero", n);
    }

    let m = if n < 0 {
        2.0
    } else {
        3.0
    };
    println!("{}", m);
}

loop

Rust提供了一个loop关键字来表示无限循环

break语句可用于随时退出循环,而continue语句可用于跳过其余的迭代并开始新的循环

rust 复制代码
// 计算 1 + 2 + ... + 100
fn main() {
    let mut sum = 0;
    let mut n = 0;
    loop {
        sum += n;
        n += 1;
        if n>100 {
            break;
        }
    }
    println!("{}", sum);
}

break后可带上一个值,例如

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };

    assert_eq!(result, 10);
    // println!("{}", result);
}

上面这种写法一般用于重试操作

while

while是带循环条件的loop.当条件为假时,结束循环.我们使用一个例子介绍while的语法:

fzzbuzz是一个非常简单的编程任务,它的描述是:

编写一个程序,打印从1到100的数字,对于3的倍数,打印Fizz而不是数字,对于5的倍数,打印Buzz

rust 复制代码
fn main() {
    // A counter variable
    let mut n = 1;
    // loop while `n` is less then 101
    while n < 101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        };
        // Increment counter
        n += 1;
    }
}

For range

Rust中的for...in...语法可以用来遍历一个选代器.有多种方式可以创建一个选代器,最简单也是最常

用的方式如下所示:

  • a...b:这将创建一个包含a而不包含b,步长为1的迭代器.
  • a...=b:这将创建一个包含a且包含b,步长为1的迭代器.
rust 复制代码
fn main() {
    //下面的代码将打印出0,1,2,3,4
    for i in 0..5 {
        println!("{}", i);
    }
    // 下面的代码将打印出0,1,2,3,4,5
    for i in 0..=5 {
        println!("{}", i);
    }
}

for...in...语法的第二个重要使用场景是遍历数组,但这需要我们首先将数组转换为一个迭代器,这可以通过.iter()或.iter_mut()实现,区别在于后者是可变的

rust 复制代码
fn main() {
    let mut myarray = [1, 2, 3];
    for i in myarray.iter() {
        println!("{}", i);
    }
    for i in myarray.iter_mut() {
        *i *= 2;
    }
    for i in myarray.iter() {
        println!("{}", i);
    }
}

Match

match是Rust中的模式匹配语法,它允许开发者将一个值与一系列模式进行比较,然后根据模式匹配的结果执行特定的代码。它与其它语言中的switch...case...语法相近,但显然更加强大。在先前的课程中,我们已经知道match语法可以配合enum一起使用

rust 复制代码
enum Alphabet { A, B }

fn main() {
    let letter = Alphabet::A;
    match letter {
        Alphabet::A => { println!("It's A"); }
        Alphabet::B => { println!("It's B"); }
    }
}

另一方面,match也经常用来匹配整型数据,例如当我们想知道一个u8整数是否是某几个特殊数字时:

rust 复制代码
fn main() {
    let n: u8 = 42;
    match n {
        42 => {
            println!("bingo!");
        }
        _ => {
            println!("{}", n);
        }
    }
}

if let

if let是Rust中的一个语法糖,它主要简化了match操作。如果我们仅仅想当匹配发生时做某些操作,那么就可以使用iflet替代match

例如当我们只想要变量letter为A时,打印消息,而忽略所有其它选项。可分别使用match或if let实现

rust 复制代码
enum Alphabet { A, B }

fn main() {
    let letter = Alphabet::A;

    match letter {
        Alphabet::A => {
            println!("It's A");
        }
        _ => {}
    }
    if let Alphabet::A = letter {
        println!("It's A");
    }
}

if let同样可以匹配带参数的枚举

rust 复制代码
enum Symbol { Char(char), Number }

fn main() {
    let symbol = Symbol::Char('A');

    if let Symbol::Char(char) = symbol {
        println!("{:?}", char);
    }
}

while let

与if let相似的还有一个while let语法糖,只是while let语法糖很少被使用

rust 复制代码
enum Alphabet { A, B }

fn main() {
    let mut letter = Alphabet::A;
    while let Alphabet::A = letter {
        println!("It's A");
        letter = Alphabet::B;
    }
}

函数与方法

函数

函数的定义以fn开始,它的参数是带类型注释的,就像变量一样,如果函数返回值,则必须在箭头 ->之后指定返回类型.例如如下的斐波那契函数:

rust 复制代码
fn main() {
    println!("{}", fibonacci(10));
}

fn fibonacci(n: u64) -> u64 {
    if n < 2 {
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

方法

方法是附加到对象的函数,这些方法可以通过self关键字访问对象及其其他方法的数据。方法在impl块下定义。访问对象中的方法有两种方式,如果方法带self参数,使用 .,否则使用 ::

rust 复制代码
#[derive(Debug)]
struct Point {
    x: u64,
    y: u64,
}

impl Point {
    fn new(x: u64, y: u64) -> Point {
        Point { x, y }
    }

    fn get_x(&self) -> u64 {
        return self.x;
    }

    // 如果需要修改结构体中的数据,self 前面需要带上 mut
    fn set_x(&mut self, x: u64) {
        self.x = x;
    }
}

fn main() {
    let mut p = Point::new(10, 20);
    println!("{:?}", p);
    println!("{:?}", p.get_x());

    p.set_x(30);
    println!("{:?}", p);
}

函数与闭包

Rust的闭包是一种匿名函数,它可以从它的上下文中捕获变量的值.闭包使用 || ->语法定义。闭包可以被保存在变量中:

rust 复制代码
fn main() {
    let myclosures = |n: u32| -> u32{ n * 3 };
    println!("{}", myclosures(1));
}

move关键字可以从闭包的运行环境中捕获值,它最常用的场景是将主线程中的一个变量传递到子线程中,如下所示:

rust 复制代码
use std::thread;

fn main() {

    //move:将环境中的值移到闭包内部
    // 使用场景-多线程:从主线程移动值到子线程
    let hello_message = "Hello World!";
    thread::spawn(move || println!("{}", hello_message)).join();
}

高阶函数

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

  • 受一个或多个函数作为输入
  • 输出一个函数

在数学中它们也叫做算子(运算符)或泛函。高阶函数是函数式编程中非常重要的一个概念

将函数作为参数传递
rust 复制代码
fn calc(method: fn(u32, u32) -> u32, a: u32, b: u32) -> u32 {
    method(a, b)
}

fn add(a: u32, b: u32) ->u32 {
    a + b
}

fn sub(a:u32, b:u32) -> u32{
    a-b
}

fn main() {
    println!("{}", calc(add, 10, 20));
    println!("{}", calc(sub, 30, 20));
}
将函数作为返回值
rust 复制代码
type Method = fn(u32, u32) -> u32;

fn calc(method: &str) -> Method {
    match method {
        "add" => add,
        "sub" => sub,
        _ => unimplemented!(),
    }
}

fn add(a: u32, b: u32) -> u32 {
    a + b
}

fn sub(a: u32, b: u32) -> u32 {
    a - b
}

fn main() {
    println!("{}", calc("add")(10, 20));
    println!("{}", calc("sub")(30, 20));
}

发散函数

发散函数永远不会被返回,它们的返回值被标记为 !,这是一个空类型。

rust 复制代码
fn foo() -> ! {
    panic!("This call never returns.")
}

发散函数与空返回值函数不同,空返回值函数可以被返回:

rust 复制代码
fn some_fn() {
    ()
}

fn main() {
    let a: () = some_fn();
    println!("This function returns and you can see this line.");
}

发散函数最大的用处是通过Rust的类型检查系统,例如,在前面的小节中我们知道Rust的if-else表达式必须返回相同的类型,但是如果使用发散函数,下面的代码也是能通过编译的:

rust 复制代码
fn foo() -> ! {
    panic!("This call never returns.")
}

fn main() {
    let a = if true { 10 } else { foo() };
    println!("{}", a);
}

猜数字游戏

这道题来自Rust的官方文档。我们将实现一个经典的初学者编程问题:猜谜游戏。它的工作原理是:程序将生成一个介于1和100之间的随机整数,然后提示玩家输入猜测。输入猜测后,程序将指示猜测是过低还是过高。如果猜测正确,游戏将打印一条祝贺信息并退出

Cargo.toml

toml 复制代码
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "*"
rust 复制代码
use std::io;

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_number: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue
        };

        println!("You guessed: {}", guess);

        // 判断大小
        if guess_number > secret_number {
            println!("Too big");
        } else if guess_number < secret_number {
            println!("Too small");
        } else {
            println!("You win!");
            break;
        }
    }
}

Rust中的模块化编程

自上个世纪90年代以来,软件工程的复杂性越来越高,程序渐渐从一个人的独狼开发转为多人团队协作开发。在今天,通过Github或中心化的代码分发网站,我们可以轻松的在一个软件工程中同时引l入世界各地的开发者开发的代码,我们与同事在同一个工程目录下并行开发不同的程序功能,或者在不拷贝代码的前提下将一个工程中的代码在另一个工程中复用这一切都是因为模块化编程

模块化编程,是强调将计算机程序的功能分离成独立的和可相互改变的"模块"的软件设计技术,它使得每个模块都包含着执行预期功能的一个唯一方面所必需的所有东西复杂的系统被分割为小块的独立代码块。

Rust项目的代码组织包含以下三个基本概念:

  • package(包)
  • Crate(箱)
  • Module(模块)

Package

Package用于管理一个或多个Crate.创建一个Package的方式是使用cargonew,我们来看看一个Package包含哪些文件

$ cargo new my-project
	Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

当我们输入命令时,Cargo创建了一个目录以及一个Cargo.toml文件,这就是一个Package。默认情况下,src/main.rs是与Package同名的二进制Crate的入口文件,因此我们可以说我们现在有一个my-project Package以及一个二进制my-projectCrate。同样,如果在创建Package的时候带上 --lib,那么 src/lib.rs将是它的Crate入口文件,且它是一个库Crate。

如果我们的src目录中同时包含 main.rslib.rs,那么我们将在这个Package中同时得到一个二进制Crate和一个库Crate,这在开发一些基础库时非常有用,例如你使用Rust中实现了一个MD5函数,你既希望这个MD5函数能作为库被别人引I用,又希望你能获得一个可以进行MD5计算的命令行工具:那就同时添加 main.rslib.rs吧!

Crate

Crate是Rust的最小编译单元,即Rust编译器是以Crate为最小单元进行编译的.Crate在一个范围内将相关的功能组合在一起,并最终通过编译生成一个二进制或库文件。例如,我们在上一章中实现的猜数字游戏就使用了rand依赖,这个rand就是一个Crate。

Module

Module允许我们将一个Crate中的代码组织成独立的代码块,以便于增强可读性和代码复用.同时,Module还控制代码的可见性,即将代码分为公开代码和私有代码.公开代码可以在项目外被使用,私有代码则只有项目内部的代码才可以访问.定义一个模块最基本的方式是使用mod关键字:

rust 复制代码
mod mod1 {
	pub mod mod2 {
		pub const MESSAGE: &str = "Hello World!"
		// ...
	}
	// ...
}

fn main () {
	println!(mod1::mod2::MESSAGE);
}

可见性

Rust中模块内部的代码,结构体,函数等类型默认是私有的,但是可以通过pub关键字来改变它们的可见性。通过选择性的对外可见来隐藏模块内部的实现细节

比较常用的三种pub写法:

  • pub:成员对模块可见
  • pub(self):成员对模块内的子模块可见
  • pub(crate):成员对整个crate可见

如果不使用pub声明,成员默认的可见性是私有的

rust 复制代码
mod mod1 {
    pub const MESSAGE: &str = "Hello World!";
    const NUMBER: u32 = 42;

    pub(self) fn mod1_pub_self_fn() {
        println!("{}", NUMBER);
    }

    pub(crate) enum CrateEnum {
        Item = 4,
    }

    pub mod mod2 {
        pub const MESSAGE: &str = "Hello World!";

        pub fn mod2_fn() {
            super::mod1_pub_self_fn();
        }
    }
}

fn main() {
    println!("{}", mod1::mod2::MESSAGE);
    println!("{}", mod1::CrateEnum::Item as u32);
    mod1::mod2::mod2_fn()
}

结构体的可见性

结构体中的字段和方法默认是私有的,通过加上pub修饰语可使得结构体中的字段和方法可以在定义结构体的模块之外被访问。要注意,与结构体同一个模块的代码访问结构体中的字段和方法并不要求该字段是可见的

rust 复制代码
mod mod1 {
    pub struct Person {
        pub name: String,
        nickname: String,
    }

    impl Person {
        pub fn new(name: &str, nickname: &str) -> Self {
            Person {
                name: String::from(name),
                nickname: String::from(nickname),
            }
        }
        pub fn say_nick_name(&self) {
            println!("{}", self.nickname);
        }
    }
}

fn main() {
    let p = mod1::Person::new("jack", "baby");
    println!("{}", p.name);
    // println!("{}",p.nickname); // 不能访问 nickname
    p.say_nick_name();
}

使用use绑定模块成员

rust 复制代码
use std::fs as stdfs;

fn main() {
    let data = stdfs::read("src/main.rs").unwrap();
    println!("{}", String::from_utf8(data).unwrap());
}

使用super与self简化模块路径

除了使用完整路径访问模块内的成员,还可以使用super与self关键字相对路径对模块进行访问

  • super:上层模块
  • self:当前模块

当上层模块,当前模块或子模块中拥有相同名字的成员时,使用super与self可以消除访问时的歧义

rust 复制代码
fn function() {
    println!("function");
}

pub mod mod1{
    pub fn function() {
        super::function();
    }
  
    pub mod mod2{
        fn function() {
            println!("mod1::mod2::function");
        }
  
        pub fn call() {
            self::function();
        }
    }
}

fn main() {
    mod1::function();
    mod1::mod2::call();
}

项目目录层次结构

将模块映射到文件

使用 mod<路径>语法,将一个rust源码文件作为模块内引l入:

src
|__main.rs
|__mod1.rs

mod1.rs

rust 复制代码
pub const MESSAGE: &str = "hello World!";

main.rs

rust 复制代码
mod mod1;

fn main() {
    println!("{}", mod1::MESSAGE);
}

将模块映射到文件夹

当一个文件夹中包含 mod.rs文件时,该文件夹可以被作为一个模块

src
|__mian.rs
|__mod1
   |__mod.rs
   |__mod1_a.rs
   |__mod1_b.rs

mod1/mod.rs

rust 复制代码
pub mod mod1_a;
pub mod mod1_b;

pub const MESSAGE: &str = "hello World!";

mod1/mod.rs

rust 复制代码
pub const NUMBER: u32 = 42;

main.rs

rust 复制代码
mod mod1;

fn main() {
    println!("{}", mod1::MESSAGE);
    println!("{}", mod1::mod1_a::NUMBER);
}

泛型作为函数参数的类型

考虑以下问题:编写一个函数,这个函数接收两个数字,然后返回较大的那个数字

rust 复制代码
fn largest(a: u32, b: u32) -> u32 {
    if a > b {
        a
    } else {
        b
    }
}

这个函数能工作,但它只能比较两个u32类型数字的大小。现在除了想比较两个u32外,还想比较两个f32。有一种可以行的办法,我们可以定义多个largest函数,让它们分别叫做 largest_u32largest_f32...这能正常工作,但不太美观。我们可以使用泛型语法对上述代码进行修改:

rust 复制代码
// type
fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    println!("{}", largest(10, 20));
    println!("{}", largest(10.0, 20.0));
    let a: f64 = 3.14;
    let b: f64 = 2.71;
    println!("{}", largest(a, b));
}

结构体中的泛型

我们还可以使用泛型语法定义结构体,结构体中的字段可以使用泛型类型参数。下面的代码展示了使用 Point<T>结构来保存任何类型的×和y坐标值

rust 复制代码
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

上述代码创建了一个×和y都是同一类型的Point结构体,但同时一个结构体中也可以包含多个不同的泛型参数:

rust 复制代码
struct Point<T, U> {
    x: T,
    y: T,
    z: U,
}

fn main() {
    let integer = Point { x: 5, y: 10, z: 15.0 };
    let float = Point { x: 1.0, y: 4.0, z: 8 };
}

但是要注意,虽然一个结构体中可以包含任意多的泛型参数,但我仍然建议拆分结构体以使得一个结构体中只使用一个泛型参数。过多的泛型参数会使得阅读代码的人难以阅读

结构体泛型的实现

我们可以在带泛型的结构体上实现方法,它的语法与普通结构体方法相差不大,只是要注意在它们的定义中加上泛型类型

rust 复制代码
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };
    println!("p.x = {}", p.x());
}

我们也可以在某种具体类型上实现某种方法,例如下面的方法将只在 Point<f32>有效

rust 复制代码
impl<T> Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

使用Traits定义共同的行为

某一类数据可能含有一些共同的行为:例如它们能被显示在屏幕上,或者能相互之间比较大小...我们将这种共同的行为称作Traits。我们使用标准库 std::fmt::Display这个traits举例,这个traits实现了在 Formatter中使用空白格式的功能

rust 复制代码
pub trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
rust 复制代码
use std::fmt;
use std::fmt::Formatter;

struct Point {
    x: i32,
    y: i32,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let origin = Point { x: 0, y: 0 };
    assert_eq!(format!("The origin is: {}", origin), "The origin is: (0, 0)");
}

使用Traits作为参数类型

在知道如何定义和实现Traits后,我们就可以探索如何使用Traits来定义接受许多不同类型的函数。这一切都与Java中的接口概念类似,也就是所谓的鸭子类型。事实上它们的使用场量也基本上是类似的

use std::fmt;
use std::fmt::Formatter;

struct Point<T> {
    x: T,
    y: T,
}

impl<T: fmt::Display> fmt::Display for Point<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn show(a: impl fmt::Display) {
    println!("show: {}", a);
}

fn main() {
    let origin = Point { x: 0, y: 0 };
    show(origin);
}

自动派生

Rust编译器可以自动为我们的结构体实现一些Traits,这种自动化技术被称作派生。例如,在编写代码的过程中最常见的一个需求就是将结构体输出的屏幕上,除了使用上节课提到的手工实现的Display,也可以采用自动派生技术让Rust编译器自动帮您添加代码:

rust 复制代码
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };
    println!("{:?}", point);
}

DebugTrait允许将数据结构使用 {:?}格式进行格式化

自动派生有一个前提是,该结构体中全部字段都实现了指定的Trait,例如,上面例子中的i32和i64就已经实现了Debug Trait

现在,我们来为Point实现另一个Trait:PartialEq。该特征允许两个数据使用 ==进行比较

rust 复制代码
#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };
    println!("{:?}", point);
}

所有权

Rust的内存管理模型

所有权是Rust这门编程语言的核心概念,Rust最引以为豪的内存安全就建立在所有权之上

所有的编程语言都存在某种管理内存的机制,拿C语言来说,这种机制是malloc和free。这意味着开发者要手动管理内存。对于编程高手而言,这是一种拥有无限可能性的技术,但对于大多数普通人而言,它是一个Bug制造机器,一些语言采用了垃圾回收技术来管理内存,也就是说开发者可以只申请内存而不用手动去释放内存,然后,垃圾回收器,也就是GC,会自动检测某块内存是否已经不再被使用,如果是的话,那么释放这块内存。但是因为GC的存在导致程序性能天生的下降,还有就是GC对程序运行带来的不确定性,任何使用GC的语言几乎不可能用来编写底层程序。我们这里说的底层是指贴近硬件的软件应用,例如操作系统和硬件驱动

在生活中,如果有两种合理但不同的方法时,你应该总是研究两者的结合,看看能否找到两全其美的方法。我们称这种组合为杂合(hybrid)。例如,为什么只吃巧克力或简单的坚果,而不是将两者结合起来,成为一块可爱的坚果巧克力呢?

Rust采用了一种中间方案RAII,它兼具GC的易用性和安全性,同时文有极高的性能

栈和堆

在开始之前,我们先来回顾一下堆和栈的区别。栈是一种先进先出的数据结构,栈内的每个元素都有固定的大小,通常是你机器CPU的位宽。例如,如果你现在在使用64位机器,那么你机器上运行的任何程序的栈的宽度就是64位,正好是一个寄存器的大小。另一方面,如果我们要放置某个对象,例如一个字符串,由于字符串的长度是不固定的,因此无法被放置在栈中。此时我们必须使用堆,而当我们想要在堆上分配一个对象,我们向操作系统请求给定的内存数量,操作系统会在可用堆中找到一个空闲位置,然后讲标记设置为已占用,并返回指向该存储位置的指针,因此堆的组织性较差,它比栈要慢,但很多时候它是唯一的处理这些动态结构的方法。下图展示了一个字符是如何存储在内存中的:变量s保存在栈中,其值是一个指向堆的地址,堆中则保存了字符串的具体内容

所有权

我们要谈谈关于所有权的实际规则

  • Rust中每个值都绑定有一个变量,称为该值的所有者
  • 每个值只有一个所有者,而且每个值都有它的作用域
  • 一旦当这个值离开作用域,这个值占用的内存将被回收
rust 复制代码
fn main() {
    let value1 = 1;
    println!("{}", value1);

    {
        let value2 = 2;
    }
    println!("{}", value2);
}
rust 复制代码
fn main() {
    let s1 = String::from("Hello World!");
    let s2 = s1;
    println!("{}", s1);
}
rust 复制代码
fn main() {
    let s2: String;

    {
        let s1 = String::from("Hello World");
        s2 = s1;
        // println!("{}", s1);
}

    println!("{}", s2);
}

借用

在有些时候,我们希望使用一个值而不拥有这个值.这种需求在函数调用时特别常见,思考以下代码:

rust 复制代码
fn echo(s: String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("HelloWorld!");
    echo(s);
    println!("{}", s);
}

编译将得到一个错误,我们不能再使用变量s,应为s的值已经被转移到函数echo了

shell 复制代码
error[E0382]: borrow of moved value: `s`
 --> src\main.rs:8:20
  |
6 |     let s = String::from("hello");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
7 |     echo(s);
  |          - value moved here
8 |     println!("{}", s);
  |                    ^ value borrowed here after move

函数echo并不想要拥有"HelloWorld!",它只是想去临时使用以下它.这类功能通过使用引l用来提供通过引用,我们可以"借用"一些值,而无需拥有它们

rust 复制代码
fn echo(s: &String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("HelloWorld!");
    echo(&s);
    println!("{}", s);
}

可变引用的规则

可变引用具有一个最重要的规则:同一时间至多只能存在一个可变引用。此规则主要用于防止数据竞争

rust 复制代码
fn main() {
    let s = String::from("HelloWorld!");
    let s1_ref = &mut s;
    let s2_ref = &mut s; // cannot borrow as mutable
}

生命周期注解

在绝大多数情况下,Rust编译器可以自动推导每个变量的生命周期。但有时候也需要我们手动在代码中注明生命周期。比较常见的场景是与&str交互的时候。生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号*(')开头,其名称通常全是小写,类似于泛型其名称非常短。'a是大多数人默认使用的名称。生命周期参数注解位于引用的&之后,并有一个空格来将引用类型与生命周期注解分隔开

例1:接受两个字符串并返回字典序较大的字符串的函数

rust 复制代码
fn bigger<'a>(str1: &'a str, str2: &'a str) -> &'a str {
    if str1 > str2 {
        str1
    } else {
        str2
    }
}

fn main() {
    println!("{}", bigger("a", "b"));
}

要注意的是,生命周期注解并不改变任何引用的生命周期的长短

例2:定义存在一个&str类型字段的结构体

rust 复制代码
#[derive(Debug)]
struct Person {
    name: &str,
}

fn main() {
    let p = Person { name: "jack" };
    println!("{:?}", p);
}
rust 复制代码
#[derive(Debug)]
struct Person<'a> {
    name: &'a str,
}

fn main() {
    let p = Person { name: "jack" };
    println!("{:?}", p);
}

不可恢复错误

使用 panic!宏是创建不可恢复的错误最简便的用法

rust 复制代码
fn main() {
    panic!("error!");
    println!("here");
}

同时还有一些常见的宏可导致不可恢复的错误

断言

rust 复制代码
fn main() {
    assert!(1 == 2);
    assert_eq!(1, 2);
}

未实现的代码

rust 复制代码
fn add(a: u32, b: u32) -> u32 {
    unimplemented!();
}

fn main() {
    println!("{}", add(1, 2));
}

不应当被访问的代码

rust 复制代码
fn divide_by_three(x: u32) -> u32 {
    for i in 0.. {
        if 3 * i < i {
            panic!("u32 overflow");
        }
        if x < 3 * i {
            return i - 1;
        }
    }
    unreachable!();
}

fn main() {
    divide_by_three(100);
}

可恢复的错误

rust 复制代码
fn main() {
    let a: Result<u32, &'static str> = Result::Ok(1);
    println!("{:?}", a);

    let b: Result<u32, &'static str> = Result::Err("result error");
    println!("{:?}", b);
}
rust 复制代码
fn main() {
    let r = std::fs::read("/tmp/foo");
    match r {
        Ok(data) => { println!("{:?}", std::str::from_utf8(&data).unwrap()); }
        Err(err) => { println!("{:?}", err); }
    }
}

自定义错误与问号表达式

问号表达式

许多时候,尤其是在我们编写库的时候,不仅仅希望获取错误,更希望错误可以在上下文中的进行传递有一种简便的方式可以传递错误:使用问号表达式.当函数的错误类型与当前错误的类型相同时,使用 ?可以直接将错误传递到函数外并终止函数执行.

rust 复制代码
fn foo() -> Result<T, E> {
    let x = bar()?; // bar 的错误类型需要与 foo 的错误类型相同
}

?的作用是将Result枚举的正常的值直接取出,如果有错误就将错误返回出去

创建自定义的错误

#[derive(Debug, PartialEq, Clone, Copy, Eq)]
pub enum Error {
    IO(std::io::ErrorKind),
}

impl From<std::io::Error> for Error {
    fn from(error: std::io::Error) -> Self {
        Error::IO(error.kind())
    }
}

fn do_read_file() -> Result<(), Error> {
    let data = std::fs::read("/tmp/foo")?;
    let data_str = std::str::from_utf8(&data).unwrap();
    println!("{:?}", data_str);
    Ok(())
}

// fn main() {
//     do_read_file().unwrap();
// }
fn main()->Result<(), Error> {
    do_read_file()?;
    do_read_file()?;
    do_read_file()?;
    Ok(())
}

Box智能指针

Box允许将一个值放在堆上而不是栈上,留在栈上的则是指向堆数据的指针.Box是一个指向堆的智能指针,当一个Box超出作用域时,它的析构函数被调用,内部对象被销毁,堆上的内存被释放

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

Box没有运行上的性能损失,虽然如此,但它却只在以下场景中比起默认的栈上分配更适用:

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候

    rust 复制代码
    enum List {
        Cons(i32, Box<List>),
        Nil,
    }
    
    fn main() {
        let list = List::Cons(0, Box::new(List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))))));
    }
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候

    rust 复制代码
    fn main() {
        let a = [0; 1024 * 512];
        let a_box = Box::new(a);
    
        // 等价写法
        // let a_box = Box::new([0; 1024 * 512]);
    }
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait而不是其具体类型的时候

    rust 复制代码
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let f = std::fs::read("/tmp/not_exist")?;
        Ok(())
    }

Rc引用计数

你可以将Rc看作Box的高级版本:它是带引用计数的智能指针.只有当它的引用计数为0时,数据才会被清理

考虑我们上节课讲的ConsList场景,如果多个节点共享一个节点,例如:

0 -> 1 \
	|-> 4
2 -> 3 /

节点4它所拥有的值会有多个所有者,这个时候就需要使用Rc来进行包装

rust 复制代码
use std::rc::Rc;

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

fn main() {
    // 一个值可以有多个所有者
    let four = Rc::new(List::Cons(4, Rc::new(List::Nil)));
    let zero_one = List::Cons(0, Rc::new(List::Cons(1, Rc::clone(&four))));
    let zero_one = List::Cons(2, Rc::new(List::Cons(3, four)));
}

Vector

Vector是动态大小的数组.与切片一样,它们的大小在编译时是未知的,但它们可以随时增长或收缩,向量使用3个参数表示:

  • 指向数据的指针

  • 长度

  • 容量

容量表示为向量预留了多少内存.一旦长度大于容量,向量将申请更大的内存进行重新分配

rust 复制代码
fn main() {
    let mut v: Vec<i32> = Vec::new();
    for i in 0..10 {
        v.push(i);
    }

    let mut v: Vec<i32> = vec![0, 1, 2, 3, 4];
    println!("{:?}", v.pop());
    println!("{:?}", v.len());
    println!("{:?}", v.capacity());
    v.push(9);

    for i in 0..v.len() {
        println!("v[{:?}]={:?}", i, v[i]);
    }

    for e in v.iter() {
        println!("{:?}", e);
    }

    for a in v.iter_mut() {
        *a *= 2;
    }

}

HashMap

HashMap是一种从Key映射到Value的数据结构

与Vector一样,HashMap也是可以动态调整大小的,可以使用以下方法创建一个HashMap.

rust 复制代码
use std::collections::HashMap;

fn main() {
    let mut transcript: HashMap<&str, u32> = HashMap::new();
    transcript.insert("alice", 95);
    transcript.insert("bob", 92);

    match transcript.get(&"alice") {
        None => println!("alice not found"),
        Some(data) => println!("alice {:?}", data),
    }

    match transcript.get(&"jack") {
        None => println!("jack not found"),
        Some(data) => println!("jack {:?}", data),
    }

    transcript.remove(&"alice");

    for (&name, &score) in transcript.iter() {
        println!("{:?} {:?}", name, score);
    }
}

大多数数据类型都可以作为HashMap的Key,只要它们实现了Eq和Hashtraits.

字符串类型str,&str与String的区分

Rust里表示"字符串"有多种方式,你可能已经见过str,&str与String,但它们之间有什么区别?如果我想使用"字符串"类型,我应当如何使用?

rust 复制代码
fn echo(s: &str) {
    println!("{:?}", s);
}

struct Foo {
    // name: &str,
    name: String,
}

fn main() {
    // "Hello World!这段数据它是保存在二进制文件中的。被保存在数据段的区域。
    // "Hello WorId!"它叫做字符串的字面量
    // str 类型几乎永远不会被用到
    // 我们总是会使用 &str
    // str 它代表的是在内存中(数据段,代码段,... 堆,栈)的字符串数据
    // &str 可以引用数据段中的内容,也可以是堆里面的内容...
    let s: &'static str = "Hello World!";
    let mut t = String::from(s);
    // String 类型,它拥有自己的数据
    // 可以修改
    // String 类型,它是存在堆里的。
    t.push_str("!!");
    // println!("{:?}", t);
    echo(s);
    echo(&t);
    let foo = Foo { name: String::from(s) };
    println!("{:?}", foo.name);
}

SystemTime

在程序中处理时间是一个常见的需求,我们来看下如何在Rust中处理时间相关的功能.

rust 复制代码
use std::thread::sleep;
use std::time::{Duration, SystemTime};

fn main() {
    //SystemTime是系统时间
    //通过系统调用请求操作系统返回的系统时间
    let now = SystemTime::now();
    println!("now = {:?}", now);
    // timestamp是自从 197g 年 1 月 1 日到现在的秒数
    // let timestamp = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
    // println!("timestamp = {:?}", timestamp);

    // sleep(Duration::from_secs(4));

    //ela航运里面的常见缩写
    // println!("ela = {:?}", now.elapsed().unwrap());

    let future = now.checked_add(Duration::from_secs(60));
    println!("future = {:?}", future);
}

如果你需要处理日期,可以使用第三方库chrono.

实现Brainfuck解释器与IR优化

由于fuck在英语中是脏话,Brainfuck有时被称为Brainfsck,甚至被简称为BF.它是大多数学生们学习编译器理论知识的好朋友,这一切都是因为它fucksimple.我们对JIT编译器的第一次尝试是如此的简单,甚至有点可笑.不过你想笑就笑吧,很快就会轮到编译器嘲笑你了,你会被告知自己写的解释器有多么的慢

Brainfuck是一种简单且最小的图灵完备编程语言.这种语言由八种运算符构成:

字符 含义
> 指针加一
< 指针减一
+ 指针指向的字节的值加一
- 指针指向的字节的值减一
. 输出指针指向的单元内容(ASCII码)
, 输入内容到指针指向的单元(ASCII码)
[ 如果指针指向的单元值为零,向后跳转到对应的]指令的次一指令处
] 如果指针指向的单元值不为零,向前跳转到对应的[指令的次一指令处

它几乎完全模仿自图灵纸带机,后者则是计算机的理论基础。理论上一切能被计算的问题都能通过Beainfuck被计算

Linuxx64汇编/HelloWorld

我们每天产出大量的垃圾代码,我们每个人都可以像这样简单地编写最简单的代码:

c 复制代码
#include <stdio.h>

int main() {
  int x = 10;
  int y = 100;
  printf("x + y = %d", x + y);
  return 0;
}

希望读者们都可以理解上述C代码的作用.但是,此代码在底层如何工作?我认为并非所有人都能回答这个问题,我也是,我可以用Haskell,Erlang,Go等高级编程语言编写代码,但是在它们编译后我并不知道它在底层是如何工作的因此,我决定采取一些更深入的步骤,进行记录,并描述我对此的学习过程希望这个过程不仅仅只是对我来说很有趣让我们开始吧

准备

开始之前,我们必须准备一些事情,如我所写的那样,我目前使用Ubuntu18.04,因此我的文章将针对该操作系统和体系结构.不同的CPU支持不同的指令集,目前我使用Intel的64位CPU.同时我也将使用NASM语法.您可以使用以下方法安装它:

shell 复制代码
apt install nasm

记住,NetwideAssembler(简称NASM)是一款基于英特尔x86架构的汇编与反汇编工具.这就是我们目前需要的.其他工具将在下一篇文章中介绍

NASM语法

在这里,我将不介绍完整的汇编语法,我们仅提及其庞大语法的一小部分,也是那些我们将在本文中使用到的部分.通常,NASM程序分为几个段(section),在这篇文章中,我们将遇到以下两个段:

  • 数据:data section
  • 文本:text section

数据部分用于声明常量,此数据在运行时不会更改.声明数据部分的语法为:

shell 复制代码
section .data

文本部分用于代码.该部分必须以全局声明_start开头,该声明告诉内核程序从何处开始执行.

shell 复制代码
section .text
global _start
_start:

注释以符号;开头。每条NASM源代码行都包含以下四个字段的某种组合

[label:] instruction [operands][; comment]

方括号中的字段是可选的.基本的NASM指令由两部分组成,第一部分是要执行的指令的名称,第二部分是该命令的操作数.例如:

mov rax, 48; put value 48 in the rax register

Hello World!

让我们用NASM编写第一个程序.当然,这将是传统的HelloWorld!程序.这是它的代码:

shell 复制代码
section .data
	msg db "hello world!", 0x0A

section .text
global _start
_start:
	mov rax, 1
	mov rdi, 1
	mov rsi, msg
	mov rdx, 13
	syscall
	mov rax, 60
	mov rdi, 0
	syscall

是的,它看起来一点都不像printf("Hello World!\n").让我们尝试了解它是什么以及它如何工作.首先看第一和第二行,我们定义了数据部分,并将msg常量与"Hello,World!"值放在一起.现在,我们可以在代码中使用此常量.接下来是声明文本部分和程序的入口.程序将从7行开始执行.现在开始最有趣的部分,我们已经知道mov指令是什么,它获得2个操作数,并将第二个的值放在第一位.但是这些rax,rdi等是什么?正如我们在Wikipedia中可以看到的:

中央处理器(CPU)是计算机中的硬件,它通过执行系统的基本算术,逻辑和输入/输出操作来执行计算机程序的指令.

好的,CPU会执行一些运算.但是,在哪里可以获取该运算的数据,是内存吗?从内存中读取数据并将数据写回到内存中会减慢处理器的速度,因为它涉及通过控制总线发送数据请求的复杂过程.因此,CPU具有自己的内部存储器,称为寄存器

因此,当我们编写mov rax,1时,意味着将1放入rax寄存器.现在我们知道rax,rdi,rsi等代表了什么了,但是还需要知道什么时候该使用rax什么时候使用rdi等

  • rax, 临时寄存器.当我们调用 syscall 时,rax 必须包含 syscall号码
  • rdi,用于将第1个参数传递给函数
  • rsi,用于将第2个参数传递给函数
  • rdx,用于将第3个参数传递给函数

换句话说,我们只是在调用 sys_write syscall.看看sys_write 的定义:

size_t sys_write(unsigned int fd, const char * buf, size_t count);

它具有3个参数:

  • fd,文件描述符.对于stdin,stdout和stderr来说,其值分别为0,1和2
  • buf,指向字符数组
  • count,指定要写入的字节数

我们将 1写入 rax,这意味我们要调用sys_write.完整的 syscall 列表可以在https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl找到.在完成该调用之后,将60写入 rax,这意味着我们要调用 sys_exit 退出程序,且退出码为 0

最后,让我们来构建这个程序,我们需要执行以下命令:

nasm -f elf64 -o main.o main.asm
ld -o main main.o

尝试运行这个程序吧!

什么是JIT

本文部分翻译自:https://blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html.

"JIT"一词往往会唤起工程师内心最深处的恐惧和崇拜,通常这并没有什么错,只有最核心的编译器团队才能梦想创建这种东西.它会使你联想到JVM或.NET,这些家伙都是具有数十万行代码的超大型运行时.你永远不会看到有人向你介绍"HelloWorld!级别的JIT编译器,但事实上只需少量代码即可完成一些有趣的工作.本文试图改变这一点

编写一个JIT编译器只需要四步,就像把大象装到冰箱里一样

1.申请一段可写和可执行的内存

2.将源码翻译为机器码(通常经过汇编)

3.将机器码写入第一步申请的内存

4.执行这部分内存

Hello, JIT World: The Joy of Simple JITs

事不宜迟,让我们跳进我们的第一个JIT程序.该代码是特定于64位Unix的,因为它使用了mmap()·鉴于此读者需要拥有支持该代码的处理器和操作系统.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

int main(int argc, char *argv[]) {
	unsigned char code[] = {0xb8, 0x00, 0x00, 0x00, 0x00, 0xc3};

	if (argc < 2) {
		fprintf(stderr, "Usage jit1 <integer>\n");
		return 1;
	}

	int num = atoi(argv[1]);
	memcpy(&code[1], &num, 4);

	void *mem = mmap(NULL, sizeof(code), PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0);
	memcpy(mem, code, sizeof(code));

	int (*func())() = mem;
	return func();
}

似乎很难相信上面的33行代码是一个合法的JIT.它动态生成一个函数,该函数返回运行时指定的整数,然后运行该函数.读者可以验证其是否正常运行:

gcc -o jit jit.c
./jit 42
echo $?
42

您会注意到,代码中使用mmap()分配内存,而不是使用malloc()从堆中获取内存的常规方法.这是必需的,因为我们需要内存是可执行的,因此我们可以跳转到它而不会导致程序崩溃.在大多数系统上,栈和堆都配置为不允许执行,因为如果您的代码跳转到了栈或堆,则意味着程序发生了很大的错误,这是由操作系统的内存结构决定的.更糟糕的是,利用缓冲区溢出的黑客可以使用可执行堆栈来更轻松地利用该漏洞因此,通常我们希望避免映射任何可写和可执行的内存,这也是在您自己的程序中遵循此规则的好习惯.我在上面打破了这个规则,但这只是为了使我们的第一个程序尽可能简单

dynasm

ACCU.CC

DynASM (luajit.org)

dynasm - crates.io: Rust Package Registry

mohanson/brainfuck: Brainfuck is an esoteric programming language. (github.com)

socket5proxy

argparse_rs - Rust (docs.rs)

mohanson/socks5proxy (github.com)

rust 复制代码
use std::io::{Read, Write};

fn hand(src_stream: &std::net::TcpStream, id: u64) -> Result<(), Box<dyn std::error::Error>> {
    println!("{:#08x} src addr: {}", id, src_stream.peer_addr().unwrap());
    let mut src_reader = src_stream.try_clone()?;
    let mut src_writer = src_stream.try_clone()?;

    let mut buf: Vec<u8> = vec![0; 64];
    src_reader.read_exact(&mut buf[0..1])?;
    if buf[0] != 0x05 {
        panic!("unreachable")
    }
    src_reader.read_exact(&mut buf[0..1])?;
    let nauth = buf[0];
    src_reader.read_exact(&mut buf[0..nauth as usize])?;
    src_writer.write_all(&[0x05])?;
    src_writer.write_all(&[0x00])?;
    src_reader.read_exact(&mut buf[0..1])?;
    if buf[0] != 0x05 {
        panic!("unreachable")
    }
    src_reader.read_exact(&mut buf[0..1])?;
    if buf[0] != 0x01 {
        panic!("unreachable")
    }
    src_reader.read_exact(&mut buf[0..1])?;
    if buf[0] != 0x00 {
        panic!("unreachable")
    }
    src_reader.read_exact(&mut buf[0..1])?;
    let host = match buf[0] {
        0x01 => {
            src_reader.read_exact(&mut buf[0..4])?;
            std::net::Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]).to_string()
        }
        0x03 => {
            src_reader.read_exact(&mut buf[0..1])?;
            let l = buf[0] as usize;
            src_reader.read_exact(&mut buf[0..l])?;
            String::from_utf8_lossy(&buf[0..l]).to_string()
        }
        0x04 => {
            src_reader.read_exact(&mut buf[0..16])?;
            std::net::Ipv6Addr::new(
                (buf[0x00] as u16) << 8 | buf[0x01] as u16,
                (buf[0x02] as u16) << 8 | buf[0x03] as u16,
                (buf[0x04] as u16) << 8 | buf[0x05] as u16,
                (buf[0x06] as u16) << 8 | buf[0x07] as u16,
                (buf[0x08] as u16) << 8 | buf[0x09] as u16,
                (buf[0x0a] as u16) << 8 | buf[0x0b] as u16,
                (buf[0x0c] as u16) << 8 | buf[0x0d] as u16,
                (buf[0x0e] as u16) << 8 | buf[0x0f] as u16,
            )
            .to_string()
        }
        _ => panic!("unreachable"),
    };
    src_reader.read_exact(&mut buf[0..2])?;
    let port = (buf[0] as u16) << 8 | (buf[1] as u16);
    let dst = format!("{}:{}", host, port);

    println!("{:#08x} dst addr: {}", id, dst);

    let dst_stream = std::net::TcpStream::connect(&dst)?;
    src_writer.write_all(&[0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])?;

    let mut dst_reader = dst_stream.try_clone()?;
    let mut dst_writer = dst_stream.try_clone()?;

    std::thread::spawn(move || {
        std::io::copy(&mut src_reader, &mut dst_writer).ok();
    });
    std::io::copy(&mut dst_reader, &mut src_writer).ok();

    Ok(())
}

fn main() {
    let mut c_listen = String::from("127.0.0.1:1080");

    {
        let mut ap = argparse::ArgumentParser::new();
        ap.set_description("Socks5 Proxy");
        ap.refer(&mut c_listen)
            .add_option(&["-l"], argparse::Store, "listen address");
        ap.parse_args_or_exit();
    }

    println!("Listen and server on {}", c_listen);

    let mut id = 0;
    let listener = std::net::TcpListener::bind(&c_listen[..]).unwrap();
    for stream in listener.incoming() {
        match stream {
            Ok(data) => {
                id += 1;
                std::thread::spawn(move || {
                    if let Err(err) = hand(&data, id) {
                        println!("error: {:?}", err);
                    }
                });
            }
            Err(err) => println!("error: {:?}", err),
        }
    }
}
相关推荐
唐 城13 分钟前
curl 放弃对 Hyper Rust HTTP 后端的支持
开发语言·http·rust
从善若水2 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust
gerrylon0073 小时前
rust学习: 有用的命令
rust
brrdg_sefg11 小时前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_7482309411 小时前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
SomeB1oody18 小时前
【Rust自学】6.1. 定义枚举
开发语言·后端·rust
SomeB1oody18 小时前
【Rust自学】5.3. struct的方法(Method)
开发语言·后端·rust
itas1091 天前
Rust调用C动态库
c语言·rust·bindgen·bindings·rust c绑定
SomeB1oody1 天前
【Rust自学】5.1. 定义并实例化struct
开发语言·后端·rust
m0_748236112 天前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust