d的dip1000,1

原文

DIP1000:现代系统编程语言中的内存安全

现代高级语言如D是内存安全的,可防止用户意外读写未用内存或破坏语言的类型系统.D安全子集提供保证,用@safe来保证函数内存安全.

@safe string getBeginning(immutable(char)* cString)
{
    return cString[0..3];
}

编译器拒绝编译此代码.无法知道cString的三个字符切片会产生什么结果.可能是cString[0]==\0的空串引用,或无\0结尾的1/2字符.此时,结果将是内存违规.

@safe不慢

注意,即使是低级系统编程项目也应尽量用@safe.不使用垃集也可保证安全.
如创建CC++库接口,或为了性能运行时避免垃集.但,可在完全不用垃集编写安全代码.
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.slicearg.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内容到返回值,但ptrslice继续指向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一起就像指针一样使用.

DIP1000ref函数参数,结构和联还有些特性.还会深入探讨DIP1000如何处理非@safe函数和属性自动推导.目前,计划是再写两篇文章.

posted @   zjh6  阅读(20)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示