From 535017337be4003dc3b9e6a4cac23f47f738791d Mon Sep 17 00:00:00 2001 From: zhangziyao Date: Tue, 12 Aug 2025 20:52:14 +0800 Subject: [PATCH] [proposals] upload bisheng c language proposals of static memory safety part --- clang/docs/BSC/Proposals/borrow.md | 287 ++++++++++++++++++++++++++ clang/docs/BSC/Proposals/ownership.md | 158 ++++++++++++++ clang/docs/BSC/Proposals/safe-zone.md | 150 ++++++++++++++ 3 files changed, 595 insertions(+) create mode 100644 clang/docs/BSC/Proposals/borrow.md create mode 100644 clang/docs/BSC/Proposals/ownership.md create mode 100644 clang/docs/BSC/Proposals/safe-zone.md diff --git a/clang/docs/BSC/Proposals/borrow.md b/clang/docs/BSC/Proposals/borrow.md new file mode 100644 index 000000000000..2c8af5141949 --- /dev/null +++ b/clang/docs/BSC/Proposals/borrow.md @@ -0,0 +1,287 @@ +# 借用 + +## 提案背景 + +毕昇 C 语言的所有权提案提供了有效地在编译期保证内存安全的语法和语义。但对于移动语义的类型而言,每次赋值、函数调用、函数返回都需要将所有权转移,限制了代码的表达能力。而实际上很多情况下,我们只需要表达对某个资源的借用,在借用的期间访问和使用资源,而不是真正拥有资源的所有权。 +本提案在毕昇 C 语言所有权提案的基础上增加借用提案,引入了新的类型说明符和运算符,表达借用语义,提升代码的表达能力,并在编译期保证借用的正确性,避免出现内存安全问题。 + +## 对标准 C 语法扩展的概述 + +本提案新增了一个关键字,即`borrow`。 + +新增 2 个运算符,为`_punctuator`(见 C11 Spec 的附录 A.1.7 节)新增 4 条产生式: + +``` +_punctuator: one of + .... + &const + &mut +``` + +新增 2 种一元运算符,为`_unary-operator`(见 C11 Spec 的附录 A.2.1 节)新增 4 条产生式: + +``` +_unary-operator: one of + .... + &const + &mut +``` + +新增 1 种类型说明符,为`_type-qualifier`(见 C11 Spec 的附录 A.2.2 节)新增 1 条产生式: + +``` +_type-qualifier: + .... + borrow +``` + +## 语法规则 + +1. 本提案新增了一个关键字`borrow`,是一个类型说明符,仅可用于修饰指针类型,表示这个指针是借用指针。 + +2. `owned`和`borrow`不能同时用于修饰同一个指针类型,最多只允许同时存在其中一个,即`int *owned borrow`是不允许的。 + +3. 本提案新增了两个一元运算符`&const`和`&mut`,区别于标准 C 语言的取地址运算符`&`。`&const`和`&mut`运算符只能用于可寻址的左值。按照标准 C 的定义,可寻址的左值的定义为: + + 1. identifiers; + 2. string literals; + 3. compound literals; + 4. parenthesized expression if the unparenthesized expression is an lvalue; + 5. the result of a member access (dot) operator if its left-hand argument is lvalue; + 6. the result of a member access through pointer -> operator; + 7. the result of the indirection (unary *) operator applied to a pointer to object; + 8. the result of the subscription operator ([]). + + > 注:对于毕昇 C 语言,第 7 条应扩充为:the result of the indirection (unary *) operator applied to a "owned pointer or borrow pointer or raw pointer" to object。 + +## 语义规则 + +1. 本提案在毕昇 C 所有权提案引入移动语义的基础上,对指针类型新引入了借用语义。具体而言,对于任意类型`T`,其借用指针类型有只读借用类型和可变借用类型: + + - `const T *borrow`,代表只读借用类型,对于被指向的`T`类型只有读权限,这种类型也成为不可变借用类型; + - `T *borrow`,代表可变借用类型,对被指向的`T`类型有读写权限。 + +2. 不允许对借用类型再取借用,即对于一个类型`T`,如果`T`本身是借用指针类型或`T`有借用指针类型的成员,则不能再创建对`T`类型的不可变借用`const T *borrow`及可变借用`T *borrow`,即`int *borrow *borrow`是不允许的。 + +3. 可变借用类型是移动语义,不可变借用类型是拷贝语义,以下给出一个例子: + + ```c + void test(int *borrow x, const int *borrow y) { + int *borrow x1 = x; // x 的所有权被转移给了 x1,后续不能再直接使用 x + int *borrow y1 = y; // y1 是 y 的一份拷贝,后续可继续使用 y + } + ``` + +4. 结构体类型的成员字段的类型可以是借用类型。 + +5. 借用类型及含有借用类型成员字段的结构体类型的变量在声明时必须显式初始化。 + +6. 通过`&const`和`&mut`,可以创建相应的借用指针类型,即对于一个可寻址的左值`e`,如果其类型为`T`,那么: + + - `&const e`,代表对`e`取地址,获得其不可变借用,类型为`const T *borrow`; + - `&mut e`,代表对`e`取地址,获得其可变借用,类型为`T *borrow`。 + +7. 与标准 C 中一样,对一个指针(包括裸指针、`owned`指针和`borrow`指针)做解引用然后再取借用,获得的值与指针本身的值是一样的,具体而言: + + - 对于`&const *e`,可以看作是对`*e`的值取不可变借用,但不为`*e`产生临时变量,取得的借用指针的值与`e`的值相同; + - 对于`&mut *e`,可以看作是对`*e`的值取可变借用,但不为`*e`产生临时变量,取得的借用指针的值与`e`的值相同。 + +8. 借用类型及含有借用类型的成员的结构体类型不允许作为全局变量的类型。 + +9. 借用类型及含有借用类型的成员的结构体类型不允许作为 union 类型的成员字段的类型。 + +10. `owned`修饰的类型及含有`owned`修饰的类型的成员的类型允许作为数组元素的类型。 + +11. 作为类型说明符,`borrow`也是类型的一部分,在做类型检查时,需要保证严格匹配,该规则与所有权提案中`owned`相关的规则一致。 + +12. 通过`&mut e`创建可变借用类型的指针时,对于表达式`e`,要求`e`是可修改的,具体而言: + + - 如果`e`是标识符,则要求变量名的类型没有被`const`修饰,且变量名不能是函数名; + - `e`不允许是字符串字面量,因为字符串字面量保存在常量区,不允许写; + - `e`可以是形如`(struct S { ... })`临时结构体变量; + - `e`可以是形如`p->field`的成员访问表达式,此时要求`p`是可变借用类型或指向可修改类型的`owned`指针,且`field`的类型没有被`const`修饰(多级成员字段访问的情况要求每一级成员字段都没有`const`修饰); + - `e`可以是形如`s.field`的成员访问表达式,此时要求`s`的类型没有被`const`修饰且`field`的类型没有被`const`修饰(多级成员字段访问的情况要求每一级成员字段都没有`const`修饰); + - `e`可以是形如`arr[index]`的索引表达式,此时要求`arr`是可变的; + - `e`可以是形如`*p`的解引用表达式,此时要求`p`是可变借用类型或是指向可修改类型的裸指针。 + +13. 对于全局变量,我们无法通过函数签名跟踪哪个函数读取了全局变量,哪个函数修改了全局变量。为了保证安全性,我们规定: + + - 在安全区内,只允许只读的全局变量,不允许可变全局变量; + - 在安全区内,只允许对全局变量取只读借用,不允许取可变借用。 + +14. 不允许对函数名做可变借用,只允许做不可变借用,此时可当作是对全局变量做的不可变借用,且可通过借用函数指针调用被借用的函数,一个例子如下: + + ```c + int f() { + return 0; + } + + void test() { + int (*borrow p)() = &const f; + p(); + } + ``` + +15. 对于借用指针类型,对运算符有以下规则: + + - 对于二元运算符,仅支持比较运算符,即`>`、`>`、`>=`、`<=`、`==`和`!=`; + - 对于一元运算符,仅支持`&`取址运算符、`*`解引用运算符、`!`逻辑非运算符、`->`箭头运算符,不允许对`owned`修饰的指针类型取索引和做偏移操作; + - 允许对借用指针类型使用`sizeof`和`_Alignof`,且`sizeof(T* borrow) == sizeof(T*)`,`_Alignof(T* borrow) == _Alignof(T*)`; + - 对于`->`箭头运算符,借用指针类型可以用来访问成员字段,且访问成员字段得到的类型取决于成员字段本身的类型,这与标准 C 是一致的; + - 对于`*`解引用运算符,借用指针类型可以解引用,且解引用得到的类型与借用指针类型指向的类型一致,这与标准 C 是一致的。 + +16. 对于借用指针类型,类型转换规则为: + + - 允许将指向具体类型`T`的借用类型转换为`void *borrow`类型; + - 不允许将`void *borrow`类型转换为指向具体类型`T`的借用类型; + - 借用类型和裸指针类型的转换是不安全的,只允许在非安全区通过强制类型转换完成,一个例子如下: + + ```c + unsafe void test() { + int *borrow p = (int *borrow)NULL; + } + ``` + +17. 引入变量的生命周期的概念,对不同类型的变量的生命周期进行严格定义,具体规则如下: + + - 全局变量的生命周期是整个程序的生命周期,从程序开始到退出,一直存在,且它的生命周期大于所有局部变量的生命周期; + - 对于右值局部变量,由于它是临时变量,因此其生命周期为当前语句,当语句结束的时候,它的生命周期就结束了; + - 对于左值局部变量,其生命周期的定义见下表: + + |左值类型|生命周期定义| + |---|---| + |变量|如果变量是借用指针类型,则从借用被创建开始,到它最后一次被使用结束(借用可被创建多次,因此借用指针类型变量的生命周期是分段的)
如果变量是`owned`修饰的移动语义类型,则从变量的声明开始,到它的所有权被转移走结束
如果变量是非借用指针的拷贝语义类型,则从变量的声明开始,到当前块作用域结束时结束| + |字符串字面量|在当前块作用域结束时结束| + |形如`(struct S { ... })`的临时结构体变量|在当前块作用域结束时结束| + |形如`p->field`的成员访问表达式|与`p`的生命周期相同| + |形如`s.field`的成员访问表达式|与`s`的生命周期相同| + |形如`arr[index]`的索引表达式|与`arr`的生命周期相同| + |形如`*p`的解引用表达式|与`p`的生命周期相同| + +18. 通过借用表达式(`&const e`或`&mut e`)来初始化借用指针类型变量`x`(即`x = &const e`或`x = &mut e`的时候,要求`x`的生命周期一定要小于`e`的生命周期。基于此规则,一个结构体内部的借用指针类型的成员,是不可以对这个结构体本身或者它的其它成员字段做借用的,一个例子如下: + + ```c + struct S { + int m; + const int *borrow p; + }; + + void test() { + struct S s = { .m = 0; .p = (const int *borrow)NULL }; + s.p = &const s.m; // Error,借用了结构体自身的其他成员字段 + } + ``` + +19. 创建借用时对被借用的表达式`e`的影响,具体规则如下: + + - 对表达式`e`做不可变借用`&const e`,要求此时没有有效的对`e`的可变借用; + - 在对表达式`e`做不可变借用的变量的生命周期结束前,`e`只能读不能被修改,且不能创建对`e`的可变借用,详细规则见下表: + + |不可变借用操作符|被借用表达式的状态| + |---|---| + |`&const var`|`var`只能读不能修改,也不能对`var`创建可变借用,允许对`var`创建不可变借用| + |`&const "string literal"`|临时变量永远是只读状态,因为我们不可能直接访问这个临时变量,也不可能通过创建出来的不可变借用,生成可变借用| + |`&const (struct S { ... })`|临时变量永远是只读状态,因为我们不可能直接访问这个临时变量,也不可能通过创建出来的不可变借用,生成可变借用| + |`&const p->field`|`p->field`进入只读状态,也不允许整体修改`*p`。但允许修改`p`指向的其它成员,或者对其它成员做可变借用或不可变借用| + |`&const s.field`|`s.field`进入只读状态,也不允许整体修改`s`。但允许修改`s`的其它成员,或者对其它成员做可变借用或不可变借用| + |`&const arr[index]`|`arr`进入只读状态,不允许修改`arr`及其直接或间接成员,或者对其它成员做可变借用| + |`&const *p`|`p`进入只读状态,不允许修改`p`及其直接或间接成员,或者对其它成员做可变借用| + + - 对表达式`e`做可变借用`&mut e`,要求此时没有有效的对`e`的可变借用和不可变借用; + - 在对表达式`e`做可变借用的变量的生命周期结束前,`e`进入冻结状态,`e`不能读写(包括转移所有权),也不能被借用,详细规则见下表: + + |可变借用操作符|被借用表达式的状态| + |---|---| + |`&mut var`|`var`进入冻结状态| + |`&mut "string literal"`|编译错误| + |`&mut (struct S { ... })`|临时变量进入冻结状态| + |`&mut p->field`|`p->field`进入冻结状态,不允许读写`p->field`,不允许整体修改`*p`,但允许修改`p`指向的其它成员,或者对其它成员做可变借用| + |`&mut s.field`|`s.field`进入冻结状态,不允许读写`s.field`,不允许整体修改`s`,但允许修改`s`的其它成员,或者对其它成员做可变借用或不可变借用| + |`&mut arr[index]`|`arr`进入冻结状态,不允许读写`arr`以及它的成员| + |`&mut *p`|`p`进入冻结状态,不允许读写`p`以及它的成员| + +20. 结构体类型的成员字段的类型允许为借用指针类型,且允许同时有多个成员字段为借用指针类型,这种情况下该结构体类型的变量同时存在多个借用的值,一个例子如下: + + ```c + struct S { + int a; + }; + + struct R { + struct S *borrow m1; + struct S *borrow m2; + }; + + void test1() { + struct S x1 = { 1 }, x2 = { 2 }; + struct R r = { .m1 = &mut x1, .m2 = &mut x2 }; + // 在 r 的生命周期结束前,x1 和 x2 一直处于冻结状态 + // 因为变量 r 在声明时同时创建了对 x1 和 x2 的可变借用 + // 导致 r 同时包含对 x1 的可变借用,也包含对 x2 的可变借用 + } + + struct Z { + const int *borrow p; + }; + + void test2() { + int x = 10; + struct Z z = { &const x }; + struct Z *borrow zp = &mut z; // Error,不允许对包含借用的表达式类型再取借用 + } + ``` + +21. 对于函数签名中的借用指针类型,其检查规则如下: + + - 如果函数的参数中没有借用类型,而函数的返回值类型为借用类型,则编译报错; + - 如果函数的参数中有一个借用类型,且函数的返回值类型为借用类型,则我们直接认为返回值类型的借用是来自于这个借用类型的参数,即返回的借用的借用值与这个借用类型参数的借用值是一样的,一个例子如下: + + ```c + int *borrow f(struct S *borrow arg) { ... } + + void test() { + struct S x = { ... }; + int *borrow p = f(&mut x); + // 在 p 的生命周期结束之前,x 一直被冻结 + // 因为函数 f 的参数创建了一个对 x 的可变借用,这个借用被传递给了返回值 p, + // 导致 p 相当于是对 x 的一个可变借用 + } + ``` + + - 如果函数的参数中有多个借用类型,且函数的返回值类型为借用类型,则我们直接认为返回值类型的借用同时来自于这多个借用类型的参数,一个例子如下: + + ```c + int *borrow f(struct S *borrow arg1, struct S *borrow arg2) { ... } + + void test() { + struct S x1 = { ... }; + struct S x2 = { ... }; + int *borrow p = f(&mut x1, &mut x2); + // 在 p 的生命周期结束之前,x1 和 x2 一直被冻结 + // 因为函数 f 的参数创建了一个对 x1 和 x2 的可变借用,返回值是 p + // 导致 p 可能是对 x1 的一个可变借用,也可能是 x2 的可变借用 + } + ``` + +22. 对于借用指针类型的解引用操作,有如下规则(`p`是`T`类型的借用指针类型变量,`o`是`T`类型的左值): + + ||`T`是拷贝语义|`T`是移动语义| + |---|---|---| + |`p`是不可变借用|`*p = expr`,不允许
`o = *p`,允许|`*p = expr`,不允许
`o = *p`,不允许| + |`p`是可变借用|`*p = expr`,允许
`o = *p`,允许|`*p = expr`,允许
`o = *p`,不允许| + + > 注:表格中的赋值操作仅为示例,同样适用于函数的传参和返回。 + +23. 对于借用指针类型的成员访问,有如下规则(`p`是借用指针类型,`p->field`的类型为`T`,`o`是`T`类型的左值): + + ||`T`是拷贝语义|`T`是移动语义| + |---|---|---| + |`p`是不可变借用|`p->field = expr`,不允许
`o = p->field`,允许|`p->field = expr`,不允许
`o = p->field`,不允许| + |`p`是可变借用|`p->field = expr`,允许
`o = p->field`,允许|`p->field = expr`,允许
`o = p->field`,不允许| + + > 注:表格中的赋值操作仅为示例,同样适用于函数的传参和返回。\ + > 注:`field`的修改权限还跟`field`本身的类型有关,如果`field`为`const`类型,则它也不能被修改,此处规则与标准 C 保持一致。\ + > 注:当`field`的类型为移动语义类型时,`p->field = expr`的情况存在内存泄漏的问题,因为`p`没有释放权限,所以不能先释放`p->field`,再赋值,这种情况会编译报错。因此为了既不导致泄漏,也不破坏借用的权限,建议使用`swap`函数来解决。\ + > 注:`swap`函数的签名为`safe void swap(T *borrow, T *borrow)`。 + +24. 不允许从不可变借用指针`p`中创建可变借用,即`&mut *p`是不允许的;允许从可变借用指针`p`中创建可变借用或不可变借用,即`&const *p`和`&mut *p`是允许的。 diff --git a/clang/docs/BSC/Proposals/ownership.md b/clang/docs/BSC/Proposals/ownership.md new file mode 100644 index 000000000000..88c13ae3c400 --- /dev/null +++ b/clang/docs/BSC/Proposals/ownership.md @@ -0,0 +1,158 @@ +# 所有权 + +## 提案背景 + +标准 C 语言只有裸指针一种指针类型,所有类型都是拷贝语义,很容易因为指针使用不当出现内存安全问题(释放后使用、多重释放、野指针、内存泄漏等),且这类问题难以在编译期准确识别到。本提案为毕昇 C 的类型引入新的类型说明符,表达所有权的语义,在编译期进行检查,避免出现此类内存安全问题。 + +具体而言,一个变量在语义上,有几种可能性: + +- 拥有所有权,意思是在这个指针所在的作用域结束的时候,我们应该做一些清理操作,比如释放内存; +- 借用,专门描述指针,意思是这个这个指针无权释放内存; +- 全局,指针指向全局空间,指针指向的位置永远不会失效。 + +一个示例如下: + +```c +// 返回一个有所有权的指针 +int *owned create(); + +// 输入一个有所有权的指针,返回一个有所有权的指针 +int *owned consume_and_return(int *owned ); + +void safe_free(void *owned); + +void test() { + int *owned p = create(); // 创建资源,这个 p 需要在某个地方释放资源 + int *owned p2 = consume_and_return(p); // p 的所有权转移给了这个函数,这个函数需要释放或者转移出去,p 在后面不可以再使用 + safe_free(p2); // 入参类型为 void *owned,我们就知道 p2 所有权移动出去了,如果没有这个调用,编译器报错 +} +``` + +## 对标准 C 语法扩展的概述 + +本提案新增了一个关键字,即`owned`。 + +新增 1 种类型说明符,为`_type-qualifier`(见 C11 Spec 的附录 A.2.2 节)新增 1 条产生式: + +``` +_type-qualifier: + .... + owned +``` + +## 语法规则 + +1. 本提案新增了一个关键字`owned`,是一个类型说明符,可用于修饰任何类型。 + +## 语义规则 + +1. 本提案在标准 C 语言拷贝语义的基础上,引入所有权语义(移动语义)。具体而言,一个变量要么是拷贝语义,要么是移动语义,它们的区别如下: + + - 拷贝语义:标准 C 语言类型的默认语义,拷贝语义的变量在赋值、传参、函数返回时会使用原始变量数据的副本创建一个新的独立变量,对创建的新的变量的修改并不会影响到原始变量,反之亦然; + - 移动语义:`owned`修饰的类型的语义为移动语义,移动语义的变量在赋值、传参、函数返回时会转移资源的所有权给新的变量,发生所有权转移后原始变量不再持有资源的所有权,因此无法再直接访问,除非再次获得资源的所有权。 + +2. `owned`允许用于修饰多级指针,且每一级指针的修饰都可以不一样,类似于标准 C 中的`const`类型说明符,`owned`也分为顶层 owned 和底层 owned,具体规则为: + + - 顶层 owned:修饰指针类型本身,如`int *owned`,表示该指针类型本身是移动语义,拥有其指向的内存的所有权; + - 底层 owned:修饰指针指向的类型,如`owned int *`,表示该指针指向的类型是移动语义,指针类型本身是拷贝语义,指针指向的类型自身拥有所有权。 + +3. `owned`修饰的类型允许作为结构体类型的成员,此时该结构体类型的`owned`修饰的类型字段为移动语义,其他类型的字段依然为拷贝语义。 + +4. 不允许出现两个`owned`修饰的指针类型指向同一块内存的情况,如果通过非安全区的操作使这种情况出现,那么后续的行为是未定义的。 + +5. `owned`修饰的类型及含有`owned`修饰的类型的成员的类型不允许作为全局变量的类型。 + +6. `owned`修饰的类型及含有`owned`修饰的类型的成员的类型不允许作为 union 类型的成员字段的类型。 + +7. `owned`修饰的类型及含有`owned`修饰的类型的成员的类型允许作为数组元素的类型,这种情况下不允许转移数组中单个元素的所有权。 + +8. 作为类型说明符,`owned`也是类型的一部分,在做类型检查时,需要保证严格匹配,具体规则为: + + - 在一个编译单元内,如果对于同一个函数有多次函数声明,那么这些函数声明的参数类型的类型说明符必须是一致的,返回值类型的类型说明符也必须是一致的,否则报编译错误,一个示例如下: + + ```c + void test_1(int *owned p); + void test_1(int *p) { // error: 参数的类型说明符与前面的声明不同 + } + ``` + + - 对于函数指针类型,在使用函数名赋值时,也需要保证参数类型和返回值类型的类型说明符与函数指针类型的一致,一个示例如下: + + ```c + typedef int (*FP)(int* owned, int *); + + int myadd(int* a, int* b); + void test() { + FP fp1 = myadd; // Error,函数签名类型不兼容 + FP fp2 = (FP)myadd; // OK,通过强制类型转换,使类型兼容 + } + ``` + + - 对于函数指针类型,如果其指向的函数类型的参数类型或返回值类型中有`owned`修饰,那么函数指针类型本身依然是裸指针类型,不会间接获得`owned`修饰,一个示例如下: + + ```c + typedef int (*FP)(int *owned, int *); + + int myadd(int *owned, int *); + + FP g_fp; // OK,FP 依然是裸指针类型,可以声明该类型的全局变量 + + void test() { + FP fp1 = myadd; // OK,fp1 为裸指针类型,fp1 指向的函数类型与 myadd 兼容 + FP owned fp2 = myadd; // Error,类型不兼容,fp2 的类型为 int (*owned)(int *owned, int *),与myadd不兼容 + FP owned fp3 = (FP owned)myadd; // OK,通过强制类型转换,使类型兼容 + } + ``` + +9. `owned`修饰指针类型时,对可使用的运算符有以下限制: + + - 对于二元运算符,仅支持比较运算符,即`>`、`>`、`>=`、`<=`、`==`和`!=`; + - 对于一元运算符,仅支持`&`取址运算符、`*`解引用运算符、`!`逻辑非运算符、`->`箭头运算符,不允许对`owned`修饰的指针类型取索引和做偏移操作。 + +10. 对于`owned`修饰的类型,其类型检查规则如下: + + - `owned`修饰的类型仅允许在非安全区被赋值、传参、返回给对应的没有`owned`修饰的类型,且该操作必须通过强制类型转换完成; + - 没有`owned`修饰的类型仅允许在非安全区被赋值、传参、返回给对应的`owned`修饰的类型,且该操作必须通过强制类型转换完成; + - 指向类型不同且不为`void`的两个`owned`修饰的指针类型不允许相互隐式转换,即如果`T1`和`T2`是不同的类型,`T1 *owned`和`T2 *owned`之间的隐式类型转换是不允许的; + - `owned`修饰的指针类型可以强制类型转换为`void *owned`类型,我们认为该操作是释放指针,且这一操作在安全区内也是允许的; + - `void *owned`类型强转为其他`owned`修饰的指针类型只能在非安全区进行。 + +11. 对于`owned`修饰的类型,在编译期做所有权检查,其检查规则如下: + + - `owned`修饰的类型的变量或结构体成员字段,在赋值、传参、返回时,是移动语义; + - `owned`修饰的类型的变量或结构体成员字段,可以通过对其赋值的操作,使其获得所有权; + - `owned`修饰的类型的变量或结构体成员字段,在赋值、传参、返回时,需要保证此时为持有所有权的状态,否则报编译错误; + - `owned`修饰的类型的变量或结构体成员字段,在赋值、传参、返回后,其持有的所有权已被转移走,再再次获得所有权前,不可再被继续使用,否则报编译错误; + - `owned`修饰的类型的变量或结构体成员字段,在其作用域结束前,必须将其持有的所有权转移走或释放掉(强转为裸指针),使其变成不持有所有权的状态,否则报编译错误,一个示例如下: + + ```c + #include + + FILE *owned open_file(const char* name) { + return (FILE *owned)fopen(name, "w+"); + } + + void use_file(FILE *owned f) { + } // Error: f 的作用域在此已经结束,但 f 的所有权并未被释放 + + void test() { + FILE *owned fp = open_file("my.log"); + use_file(fp); + use_file(fp); // Error,fp 的所有权已被转移走,此处不可再继续使用 + } + ``` + + - 对于含有`owned`修饰的类型的成员字段的结构体类型,结构体内的每个`owned`修饰的类型的成员字段的所有权状态单独跟踪; + - 如果一个结构体内部的`owned`修饰的类型的成员字段的所有权已被转移走,则这个结构体不能再作为一个整体被用于赋值、传参、返回; + - 如果一个结构体内部的`owned`修饰的类型的成员字段的所有权已被转移走,当这些字段再次都重新获得所有权后,结构体才能再作为一个整体被用于赋值、传参、返回; + - 如果一个类型`T`内部有`owned`修饰的类型的成员,那么必须要求把所有`owned`修饰的类型的成员都释放后,才能把`T *owned`释放掉(即强转为`void *owned`),否则会导致内存泄漏,要报编译错误; + - 如果把类型`T`对应的`T *owned`类型强转为`void *owned`,但没有通过变量或函数传参接收,则会导致内存泄漏,要报编译错误,一个例子如下: + + ```c + void safe_free(void *owned p); + + void test(int *owned p, int *owned q) { + (void *owned)p; // Error,虽然强转为了 void *owned,但没有变量或函数传参接收,会内存泄漏 + safe_free((void *owned)q); // OK,通过函数传参接收,无内存泄漏 + } + ``` diff --git a/clang/docs/BSC/Proposals/safe-zone.md b/clang/docs/BSC/Proposals/safe-zone.md new file mode 100644 index 000000000000..d3b560de1561 --- /dev/null +++ b/clang/docs/BSC/Proposals/safe-zone.md @@ -0,0 +1,150 @@ +# 安全区 + +## 提案背景 + +标准 C 语言有很多规则过于灵活,不方便编译器做静态检查,且这类规则难以保证程序的内存安全性。因此,我们引入一个新语法区分安全区和非安全区,使得在安全区范围内的毕昇 C 代码必须遵循更严格的约束,保证在这个范围内的代码肯定不会出现“内存安全”问题。 + +## 对标准 C 语法扩展的概述 + +本提案新增了两个关键字,分别为`safe`和`unsafe`。 + +在 C 语法基础上(对应 C11 Spec 的 附录 A.2.2 节),新增`_safe-specifier`,表示安全限定符: + +``` +_safe-specifier: + safe + unsafe +``` + +新增 2 种函数限定符,为`_function-specifier`(见 C11 Spec 的附录 A.2.2 节)新增 1 条产生式(省略号表示省略已有的语法规则): + +``` +_function-specifier: + .... + _safe-specifier +``` + +安全限定符可修饰代码块,修改了`_compound-statement`(见 C11 Spec 的附录 A.2.3 节)的第 1 条产生式(`_opt`表示是可选的非终结符): + +``` +_compound-statement: + _safe-specifier_opt { _block-item-list_opt } +``` + +## 语法规则 + +1. 本提案新增了两个关键字`safe`和`unsafe`,可用于修饰一个代码块或一个函数,它们的含义为: + + - 使用`unsafe`修饰,表示这段代码(函数体或代码块)在非安全上下文(非安全区)内,这部分代码遵循标准 C 的规定,同时这部分代码的内存安全性由用户自行保证; + - 使用`safe`修饰,表示这段代码(函数体或代码块)在安全上下文(安全区)内,这部分代码必须遵循更严格的语法和语义约束,同时这部分代码的内存安全性由编译器保证。 + +2. `safe`和`unsafe`不允许同时用于修饰一个函数或代码块,即一个函数或代码块本身要么是安全上下文,要么是非安全上下文。 + +3. `safe`和`unsafe`关键字允许修饰函数声明和函数定义及代码块,且在`safe`或`unsafe`修饰的函数定义或代码块内部,也允许再使用`safe`或`unsafe`修饰新的代码块,使得能在安全上下文和非安全上下文间转换。 + +4. `safe`和`unsafe`关键字不允许修饰全局变量的初始化表达式,因为初始化的时候不能执行复杂表达式。 + +5. `safe`和`unsafe`关键字不允许用于修饰自定义类型声明,类型是否为安全类型(见语义规则部分对安全类型和不安全类型的定义)取决于类型本身的定义,由编译器自动推理出来。 + +6. `safe`和`unsafe`关键字允许用于修饰函数签名和函数指针类型,因为它们是函数签名的一部分。 + +7. 对于`safe`修饰的函数签名,有如下要求: + + - 函数的参数列表不可省略,不需要任何参数时也需要在参数列表中加上`void`,明确表示这个函数不接受任何参数,即`safe void f()`是不允许的,而`safe void f(void)`是允许的; + - 函数的参数列表不能包含可变长参数,即`safe int printf(int format, ...)`是不允许的。 + +8. 对于`unsafe`修饰的函数签名,没有任何约束,即用`unsafe`修饰函数签名与不使用`safe`和`unsafe`修饰函数签名是等价的,都遵循标准 C 的规则。 + +9. `safe`和`unsafe`不允许用于修饰静态断言。 + + +## 语义规则 + +1. 为了与标准 C 语言兼容,默认情况下毕昇 C 假设全局上下文是非安全上下文。即默认情况下,由用户自行保证内存安全。 + +2. 为毕昇 C 引入安全类型和不安全类型的概念,一个类型要么是安全类型,要么是不安全类型。毕昇 C 的不安全类型有: + + - 裸指针类型 + - union 类型 + - 成员中包含不安全类型的 struct 类型 + - 成员类型为不安全类型的数组类型 + + 注:毕昇 C 在所有权和借用两个提案中还新引入了`owned`和`borrow`修饰的指针类型,它们不同于裸指针类型,也属于安全类型。相关内容参考所有权和借用两个提案。 + +3. 对于`safe`修饰的函数签名,要求函数的参数类型和返回值类型必须都是安全类型。 + +4. 使用`unsafe`修饰函数签名时,`unsafe`可以省略,因为默认情况下就是非安全上下文。 + +5. 在一个编译单元内,如果同名函数存在多个声明,则遵循以下规则: + + - 多个声明必须有同样的`safe`和`unsafe`修饰; + - 多个声明在排除`safe`和`unsafe`修饰后,也必须是类型兼容(compatible)的。 + +6. 对于有`safe`修饰的函数指针类型,只有在类型是完全相同(identical)的情况下,才能互相赋值;而对于没有`safe`修饰或是`unsafe`修饰的函数指针类型,则遵循标准 C 的规则,即只要类型是兼容(compitable)的,就可以互相赋值。以下给出一个例子: + + ```c + safe int f(int); + typedef safe int (*pf)(int); + + void test() { + pf g = f; // OK,类型是完全相同的 + g(1); + } + ``` + +7. 在安全上下文内发生函数调用时,被调用的函数的函数签名必须是`safe`修饰的,且要求传入的实参类型可以隐式转换为形参类型。 + +8. 在安全上下文内,对语句有如下约束: + + - 变量声明时必须要初始化,不允许无初始化的变量声明; + - 使用初始化列表做初始化时必须是完整的初始化列表,不允许做部分初始化; + - 不允许声明及读写不安全类型的变量; + - 不允许标签语句和 goto 跳转语句; + - switch 语句中的 case 和 default 只能存在于 switch 后面的第一层代码块中; + - switch 语句中的第一层代码块中不允许有变量声明; + - 不允许内联汇编语句。 + +9. 在安全上下文内,对表达式有如下约束: + + - 不允许`++`和`--`运算符; + - 不允许`&`取址运算符,只允许`&const`和`&mut`取借用运算符(详见借用提案); + - 不允许通过`.`运算符访问 union 类型的成员; + - 不允许解引用裸指针类型的变量及表达式,包括直接通过`*`运算符做解引用和通过`->`运算符访问成员; + - 不允许直接调用`unsafe`修饰的函数,只能在`unsafe`修饰的代码块(进入非安全上下文)中调用`unsafe`修饰的函数。 + +10. 在安全上下文内,对于类型转换有如下约束: + + - 对于基本数据类型,不允许从表达范围大的类型从表达范围小的类型转换,常量值除外,例如`long`转为`int`,`int`转为`_Bool`及`int`转为某个`enum`等; + - 对于基本数据类型,不允许从表达精度高的类型从表达精度低的类型转换,常量值除外,例如`double`转为`float`; + - 对于基本数据类型的常量发生的类型转换,如果目标类型可以描述当前的常量值,那么这样的转换是允许的,且不需要显式类型转换,下面给出一个例子: + + ```c + const int x = 100; + char y = x; // OK,虽然是从大到小的转换,但因为 x 的值是编译期已知的,可以判断出来 char 能描述 100,因此允许 + ``` + + - 不允许指向不同的指针类型之间的转换(其他类型的`owned`指针强制转换为`void *owned`指针除外,仅允许这一种情况,详见所有权提案); + - 不允许`owned`指针、`borrow`指针及裸指针之间的转换(`owned`指针和`borrow`指针见所有权提案和借用提案); + - 不允许裸指针类型和任何非指针类型之间的转换。 + +11. 在安全上下文内,需要禁止标准 C 中的未定义行为: + + - 同一个表达式中对同一个变量有两次修改行为,(如`int y = ++x + x++`、`int y = f(p1) + f(p2)`,两个指针指向同一个变量算不算未定义行为?) + - 不合法的 scalar value,如 _Bool 类型的变量的取值不是 0 或者 1,enum 类型的变量取值范围不在枚举范围内(应该做一些类型增强,比如 enum 可以转为整型,但不能从整型转回来) + - 没有副作用的无限循环 + +12. 在安全上下文内,不允许修改全局变量,仅允许读全局变量。 + + > 注:需要进一步补充,当前这条规则过于严格,且这条规则与“线程安全”有一定关系,有些类型是线程安全的,这些类型的全局变量可以在安全上下文被修改,如 _Atomic 类型、_Thread_local 类型。 + +13. 在非安全上下文内,允许所有标准 C 特性。 + +14. 在非安全上下文内,允许`owned`指针、`borrow`指针和裸指针之间的相互强制类型转换(`owned`指针和`borrow`指针见所有权提案和借用提案)。 + +15. 在非安全上下文内,依然会进行`owned`指针和`borrow`指针类型的相关语义检查。若希望禁止相关检查,则需要将`owned`指针和`borrow`指针强转为裸指针(详见所有权提案和借用提案)。 + +16. 提供一个编译器的编译选项,在开始这个选项的情况下,在安全上下文中插入一些额外的运行时检查: + + - 使用移位运算符时,如果有符号整数发生移位溢出,则运行时报错; + - 使用加减运算符时,如果有符号整数或指针运算发生溢出,则运行时报错; + - 使用出发运算符时,如果除数为 0,则运行时报错。 \ No newline at end of file -- Gitee