DIP1000:现代系统编程语言中的内存安全现代高级语言如D是内存安全的,可防止用户意外读写未用内存或破坏语言的类型系统.D的安全子集提供保证,用@safe来保证函数内存安全.
@safe string getBeginning(immutable(char)* cString)
{
return cString[0..3];
}
编译器拒绝编译此代码.无法知道cString的三个字符切片会产生什么结果.可能是cString[0]==\0的空串引用,或无\0结尾的1/2字符.此时,结果将是内存违规.
@safe不慢注意,即使是低级系统编程项目也应尽量用@safe.不使用垃集也可保证安全.
如创建C或C++库接口,或为了性能运行时避免垃集.但,可在完全不用垃集编写安全代码.
D可这样,是因为内存安全子集不会阻止原始内存访问.
@safe void add(int* a, int* b, int* sum)
{
*sum = *a + *b;
}
尽管按完全相同未经检查的C方式解引用这些指针,但可编译且是完全内存安全的.
因为@safeD禁止创建指向未分配内存区域的int*或float**,如int*可指向空(null)地址,但这不是内存安全问题,因为空地址受操作系统保护.
在损坏内存前,解引用它们会使程序崩溃.因为仅在请求更多内存或显式调用垃集时才运行它,不涉及垃集.
D切片类似.运行时索引时,动态检查索引是否小于长度,仅此而已.不会检查是否指向合法内存区域.内存安全通过源头上防止创建引用非法内存切片来实现的,如第一例所示.再一次,与垃集无关.
这启用了许多内存安全,高效且独立于垃集的模式.
struct Struct
{
int[] slice;
int* pointer;
int[10] staticArray;
}
@safe @nogc Struct examples(Struct arg)
{
arg.slice[5] = *arg.pointer;
arg.staticArray[0..5] = arg.slice[5..10];
arg.pointer = &arg.slice[8];
return arg;
}
如上,在@safe代码中,D自由地允许你不检查内存处理.arg.slice和arg.pointer可能在垃集堆上,也可能在静态程序内存中引用内存.语言不关心.
为指针和切片分配内存,程序可能需要垃集或管理一些不安全内存,但已分配内存则不必.如果该函数需要垃集,会因为@nogc而编译失败.
这里有个历史设计缺陷,即内存也可能在栈上.如果稍微改变函数会怎样?
@safe @nogc Struct examples(Struct arg)
{
arg.pointer = &arg.staticArray[8];
arg.slice = arg.staticArray[0..8];
return arg;
}
arg构是值类型.调用examples时,复制内容(arg构)到栈中,并且可在函数返回后,覆盖它.staticArray也是值类型.像结构中有十个整数一样,与结构的其余部分一起复制.返回arg时,复制staticArray内容到返回值,但ptr和slice继续指向arg,而不是返回的副本!
但可解决.它允许像以前一样,在函数中编写包括引用栈的@safe代码.甚至可安全(@安全)编写一些以前用@system的技巧.该修复程序是DIP1000.因此如果用最新的每晚dmd编译此示例,会默认报告已弃用警告.
DIP1000增强了指针,切片和其他引用的语言规则.dip1000.可用-preview=dip1000预览编译器开关启用新规则.
现有代码需要一些更改才能使用新规则,因而默认不启用该开关.未来将是默认,因此最好现在就启用它,并努力使代码与它兼容.
基本思想是限制引用(如数组或指针)的生命期.如果栈指针存在时间不超过指向的栈变量,则它并不危险.普通引用继续存在,但只能引用有无穷生命期数据:即垃集内存及静或全局变量.
构造有限生命期引用的最简单方法是用有限生命期对象赋值它.
@safe int* test(int arg1, int arg2)
{
int* notScope = new int(5);
int* thisIsScope = &arg1;
int* alsoScope; // 非初始域
alsoScope = thisIsScope; // 但这就是了
// 先前定义变量,有更长生命期,所以禁止!
thisIsScope = alsoScope;
return notScope; // 好
return thisIsScope; // 错误
return alsoScope; // 错误
}
测试这些示例时,记住使用-preview=dip1000编译器开关,并标记函数为@safe.因为不检查非@safe函数.
或,可显式用scope关键字来限制引用的生命期,这里.
@safe int[] test()
{
int[] normalRef;
scope int[] limitedRef;
if(true)
{
int[5] stackData2= [-1, -2, -3, -4, -5];
//stackData2在limitedRef前结束,因而禁止
limitedRef = stackData2[];
//你这样
scope int[]evenMoreLimited=stackData2[];
}
return normalRef; // 好.
return limitedRef; // 禁止.
}
如果不能返回有限生命期引用,则如何使用它们?简单.记住,仅保护数据地址,而不是数据自身.即有很多方法可从函数中传递出去域数据.
@safe int[] fun()
{
scope int[] dontReturnMe = [1,2,3];
int[] result=new int[](dontReturnMe.length);
//复制数据,而不是让结果`引用`受保护内存.
result[] = dontReturnMe[];
return result;
// 同上,简写.
return dontReturnMe.dup;
// 计算感兴趣数据
return
[
dontReturnMe[0] * dontReturnMe[1],
cast(int) dontReturnMe.length
];
}
目前,DIP1000仅处理有限生命期引用,但也可给函数参数应用scope存储类.保证了退出函数后,不会使用该内存,可按scope参数使用局部数据引用.
@safe double average(scope int[] data)
{
double result = 0;
foreach(el; data) result += el;
return result / data.length;
}
@safe double use()
{
int[10] data = [1,2,3,4,5,6,7,8,9,10];
return data[].average; // 工作!
}
开始,最好关闭自动推导属性.一般,自动推导很好,但它为所有参数安静的添加域属性,很容易忘记当前工作.从而更难学习.
始终明确指定返回类型(或void/noreturn)来避免:用@safe const(char[]) fun(int* val)而不是@safe auto fun(int* val)或@safe const fun(int* val).该函数也不必是模板或在模板内.未来研究scope自动推导.
scope允许处理栈指针和数组,但禁止返回它们.目的呢?输入return scope属性:
//作为字符数组,串也适合`DIP1000`.
@safe string latterHalf(return scope string arg)
{
return arg[$/2 .. $];
}
@safe string test()
{
//在静态程序内存中分配
auto hello1 = "Hello world!";
//在栈上分配,从`hello1`复制
immutable(char)[12] hello2 = hello1;
auto result1 = hello1.latterHalf; // 好
return result1; // 好
auto result2 = hello2[].latterHalf; // 好
//很好!result2是域,不能返回它
return result2;
}
中域参数检查传递给它们的参数是否为scope.是,则按不超过任一中域参数的scope值对待返回值.否(都不是域),则按可自由复制的全局引用对待返回值.像scope,return scope(中域)是保守的.即使不返回returnscope保护地址,编译器仍会像返回一样,检查调用点生命期.
scope是浅的@safe void test()
{
scope a = "first";
scope b = "second";
string[] arr = [a, b];
}
在test中,不会编译初化arr.因为如果需要,语言会在初化时自动加scope到变量中,这令人惊讶.
但是,请考虑scope string[] arr上的scope保护什么.可保护两样:数组中串地址,或串中字符地址.为了该赋值安全,scope必须保护串中字符,但它只保护顶级引用,即数组中串.因此,该示例不工作.现在更改arr为静态数组:
@safe void test()
{
scope a = "first";
scope b = "second";
string[2] arr = [a, b];
}
因为静态数组不是引用,这工作.在栈上就地分配所有元素内存(即,栈包含元素),与包含引用存储在其他地方元素的动态数组不一样.
静态数组为scope时,按scope对待元素.且如果arr非域,则该示例无法编译,因此可推导scope.
需要时间来理解DIP1000规则,许多人不愿学习.第一个也是最重要的技巧是:尽量避免非@safe代码.
当然,该建议并不新鲜,但对DIP1000来说更重要.总之,在非@safe函数中,语言不会检查非安全函数的域和中域有效性,而调用这些函数时,编译器会假定满足了这些属性.
因而在不安全代码中,域和中域危险.但是只要避免标记为@trusted,D代码几乎不会造成损害.@safe代码中滥用DIP1000可能会导致不必要编译错误,但它不会损坏内存和其他错误.
值得一提的第二个重点是,如果函数属性仅接收静态或GC分配数据,则不必用域和中域.
许多语言禁止程序员引用栈.D可以这样,不代表必须用栈.这样,像DIP1000出来之前,不必花费更多时间来解决编译器错误.如果想用栈,就注解函数.这样,不破坏接口.
本文,说明了在DIP1000中使用数组和指针.原则上,还允许读者用类和接口使用DIP1000.
唯一要了解的是包括成员函数中this指针的类引用,同DIP1000一起就像指针一样使用.
DIP1000对ref函数参数,结构和联还有些特性.还会深入探讨DIP1000如何处理非@safe函数和属性自动推导.目前,计划是再写两篇文章.