<-- Home |--rust |--c

Learn Rust by Code Bugs in C语言实现Rust无法做到的bug

Rust的Niche在哪里?

Rust吹得那么凶,大家那么喜欢她,人人都爱她,她的生态位到底在哪里呢?不可能说一个丑女大家都能喜欢。所以,人家还是有点东西的。

《Rust in Action》里面就分析了Rust可以解决四类编程问题:

  • 悬垂指针:引用了在程序运行过程中已经变为无效的数据
  • 数据竞争:由于外部因素的变化,无法确定程序在每次运行时的行为
  • 缓冲区溢出:例如一个只有6个元素的数组,试图访问其中的第12个元素
  • 迭代器失效:在迭代的过程中,迭代器中值被更改而导致的问题

其实Rust确实不适合当作第一门语言来学习,因为人只有写过bug才知道Rust这种Bug都不让你写有一点点价值……

Bug大师:C语言

大家都说C语言是bug大师,这个说法还是很客气的。C语言简直是bug圣斗士、bug之神。其实主要的原因就是C语言自由度非常高,电脑(计算机)能干什么,C语言就让你干,你可以随便干。语言层面上,C非常接近汇编语言乃至机器语言。在C语言中,所有的代码和内存都是平等的……

下面我们演示一下Rust写不来的bug。

悬垂指针

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4
 5int *get_arr()
 6{
 7    int x = 42;
 8    return &x;
 9}
10
11int main()
12{
13    printf("case 1: dangling pointer\n");
14    int *arr = (int *)malloc(sizeof(int) * 10);
15    for (int i = 0; i < 10; i++)
16    {
17        arr[i] = i;
18    }
19    free(arr);
20    for (int i = 0; i < 10; i++)
21    {
22
23        printf("%d ", arr[i]);
24    }
25    printf("\n");
26
27    printf("case 2: memory leak\n");
28    int *arr2 = NULL;
29    {
30        int y = 42;
31        arr2 = &y;
32    }
33    printf("%d\n", *arr2);
34
35    printf("case 3: return dangling pointer\n");
36    int *arr3 = get_arr();
37    printf("%d\n", *arr3);
38
39    return 0;
40}

要做一个悬垂指针很简单,申请一块内容,然后把它给释放了,随后就当无事发生,继续用指针访问它,其实计算机根本部不管你这些那些的,你随意编造一个指针地址去访问也是可以的……参考程序13~25行,程序可能打印出来的值还是对的呢!因为计算机也没有必要去把方式内存中的值给清空。其实,C语言还允许你随便乱编造一个不同数据类型的指针去读、写内存呢。这个玩意呢,有一个最佳实践,就是,free之后,把指针设置为NULL,这样你下次访问的时候,程序就会崩溃(!),你就知道你犯错了。

还有另外两种Rust不行的,就是把局部定义域种的变量、别的函数种的局部变量的地址(指向该变量)返回给,然后使用这个地址。

其实那个栈上的地址也总是存在的,只不过是里面的信息可能(也可能暂时还没有)被覆盖……也就是说,按照道理,对于代码来说,那个地址的信息已经从语义上不存在了……Rust就做不到这个……

数据竞争

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <threads.h>
 4
 5int running = 1;
 6int run_print = 1;
 7
 8int increment_thread(void *arg)
 9{
10    srand(42);
11    int *counter = (int *)arg;
12    while (running)
13    {
14        int delta = rand() % 10;
15        printf("increment: %d\n", delta);
16        (*counter) += delta;
17        thrd_sleep(&(struct timespec){.tv_sec = 0, .tv_nsec = 200000000}, NULL);
18    }
19    return 0;
20}
21
22int decrement_thread(void *arg)
23{
24    srand(43);
25    int *counter = (int *)arg;
26    while (running)
27    {
28        int delta = rand() % 10;
29        printf("decrement: %d\n", delta);
30        (*counter) -= delta;
31        thrd_sleep(&(struct timespec){.tv_sec = 0, .tv_nsec = 200000000}, NULL);
32    }
33    return 0;
34}
35
36int print_counter(void *arg)
37{
38    int *counter = (int *)arg;
39    while (run_print)
40    {
41        printf("counter: %d\n", *counter);
42        thrd_sleep(&(struct timespec){.tv_sec = 0, .tv_nsec = 500000000}, NULL);
43    }
44    return 0;
45}
46
47int main()
48{
49    int counter = 0;
50    thrd_t tid1, tid2, tid3;
51    thrd_create(&tid3, print_counter, &counter);
52    thrd_create(&tid1, increment_thread, &counter);
53    thrd_create(&tid2, decrement_thread, &counter);
54
55    thrd_sleep(&(struct timespec){.tv_sec = 3, .tv_nsec = 0}, NULL);
56    running = 0;
57    thrd_join(tid1, NULL);
58    thrd_join(tid2, NULL);
59    run_print = 0;
60    thrd_join(tid3, NULL);
61    printf("counter: %d\n", counter);
62    return 0;
63}

这个程序也很简单,不过编译要用c11的标准,才有threads.h

1gcc -std=c11 racing_conditions.c -o racing_conditions

如果是msvc,则需要用/std:c11

1cl /std:c11 racing_conditions.c

这里,一个局部变量counter被三个线程同时访问。两个摸一个看。一个使劲增加它,一个使劲减少它,一个使劲打印它。

要知道,Rust可不允许搞什么群体性活动……如果有一个人摸,别人看都不能看;可以同时有几个人看。

缓冲区溢出

 1#include <stdio.h>
 2
 3int main() {
 4    int* a = (int*)malloc(sizeof(int) * 3);
 5    int* b = (int*)malloc(sizeof(int) * 3);
 6    *a = 1;
 7    *b = 2;
 8    *(a + 1) = 3;
 9    *(b + 1) = 4;
10    *(a + 2) = 5;
11    *(b + 2) = 6;
12    *(a + 3) = 7;
13    *(b + 3) = 8;
14    
15    for (int i = 0; i < 4; i++) {
16        printf("a[%d] = %d\n", i, *(a + i));
17        printf("b[%d] = %d\n", i, *(b + i));
18    }
19    free(a);
20    free(b);
21    return 0;
22}

C语言根本不检查数组越界,所以,你随便越界,计算机也管不着。

Rust就不行,不能在自己加外面看、更不能摸!

迭代器失效

 1#include <stdio.h>
 2#include <threads.h>
 3
 4typedef struct Vec
 5{
 6    int data;
 7    struct Vec *next;
 8} Vec;
 9
10Vec *create_node(int data)
11{
12    Vec *new_node = (Vec *)malloc(sizeof(Vec));
13    new_node->data = data;
14    new_node->next = NULL;
15    return new_node;
16}
17
18void push_end(Vec *head, int data)
19{
20    Vec *current = head;
21    while (current->next != NULL)
22    {
23        current = current->next;
24    }
25    current->next = create_node(data);
26}
27
28Vec *next(Vec *head)
29{
30    return head->next;
31}
32
33int main()
34{
35    Vec *head = create_node(0);
36    for (Vec *current = head; current != NULL; current = next(current))
37    {
38        printf("%d\n", current->data);
39        push_end(current, current->data + 1);
40        thrd_sleep(&(struct timespec){.tv_sec = 0, .tv_nsec = 100000000}, NULL);
41    }
42    free(head);
43    return 0;
44}

因为C语言就没有什么迭代器,差不多就这个意思整一下。

在Rust中,不行,不行,一边迭代一个集合,一边修改这个集合是不行的……

Rust的开发工具

这里呢,就可以看到,学习Rust应该怎么学?找准Rust的Niche,然后,用C语言的思维能做、Rust不能做的的角度来学习Rust。

另外呢,还要介绍一下Rust的一个工具,叫做cargo,他有一个功能,叫做cargo fix,可以自动试图修复你的错误……真是离谱。

我们整一个悬垂指针:

 1#[derive(Debug)]
 2enum Cereal {
 3    Barley,
 4    Corn,
 5    Millet,
 6    Rice,
 7    Wheat,
 8}
 9
10fn main() {
11    let mut grains: Vec<Cereal> = vec![];
12    grains.push(Cereal::Barley);
13    grains.push(Cereal::Barley);
14    grains.push(Cereal::Barley);
15    grains.push(Cereal::Barley);
16    grains.push(Cereal::Barley);
17
18    drop(grains);
19
20    println!("{:?}", grains);
21}

然后,我们运行一下:

1cargo check

这其实已经说的很清楚了,甚至给出了错误的原因。

 1error[E0382]: borrow of moved value: `grains`
 2  --> src\main.rs:23:22
 3   |
 414 |     let mut grains: Vec<Cereal> = vec![];
 5   |         ---------- move occurs because `grains` has type `Vec<Cereal>`, which does not implement the `Copy` trait
 6...
 721 |     drop(grains);
 8   |          ------ value moved here
 922 |
1023 |     println!("{:?}", grains);
11   |                      ^^^^^^ value borrowed here after move
12   |
13   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
14
15For more information about this error, try `rustc --explain E0382`.
16error: could not compile `learn-rust-by-code-bugs-in-C` (bin "learn-rust-by-code-bugs-in-C") due to 1 previous error

然后,我们就可以用cargo fix来修复这个错误:

1cargo fix

它首先会提示你,别整了,先commit一下再搞。合理!

1error: the working directory of this package has uncommitted changes, and `cargo fix` can potentially perform destructive changes; if you'd like to suppress this error pass `--allow-dirty`, or commit the changes to these files:

然后,我们就可以用--allow-dirty来修复这个错误:

1cargo fix --allow-dirty

不过最好是先commit一下,然后,再cargo fix

结果,它打印出来的信息跟cargo check是一样的。

我就当无事发生,今天的天气真好。

总结

C坏,Rust好……那是不可能的。Rust编程序真的费劲,需要跟编译器使劲解释,搞清楚谁能摸、谁能看,摸的摸多久,看的看到什么时候……

Rust主要是后发优势,工具链完善,报错信息(这是因为我们需要给编译器提供大量的信息)完善。


文章标签

|-->rust |-->c |-->memory safety |-->concurrency |-->safety |-->bugs |-->悬垂指针 |-->数据竞争 |-->缓冲区溢出 |-->迭代器失效


GitHub