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 |-->悬垂指针 |-->数据竞争 |-->缓冲区溢出 |-->迭代器失效
- 本站总访问量:次
- 本站总访客数:人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository