• d的dip1000,1


    原文

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

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

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

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

    @safe不慢

    注意,即使是低级系统编程项目也应尽量用@safe.不使用垃集也可保证安全.
    如创建CC++库接口,或为了性能运行时避免垃集.但,可在完全不用垃集编写安全代码.
    D可这样,是因为内存安全子集不会阻止原始内存访问.

    @safe void add(int* a, int* b, int* sum)
    {
        *sum = *a + *b;
    }
    
    • 1
    • 2
    • 3
    • 4

    尽管按完全相同未经检查的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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如上,在@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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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; // 错误
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    测试这些示例时,记住使用-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; // 禁止.
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如果不能返回有限生命期引用,则如何使用它们?简单.记住,仅保护数据地址,而不是数据自身.即有很多方法可从函数中传递出去域数据.

    @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
        ];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    取交互过程

    目前,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; // 工作!
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    开始,最好关闭自动推导属性.一般,自动推导很好,但它为所有参数安静的添加属性,很容易忘记当前工作.从而更难学习.
    始终明确指定返回类型(或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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    中域参数检查传递给它们的参数是否为scope.是,则按不超过任一中域参数的scope值对待返回值.否(都不是),则按可自由复制全局引用对待返回值.像scope,return scope(中域)是保守的.即使不返回returnscope保护地址,编译器仍会像返回一样,检查调用点生命期.

    scope是浅的

    @safe void test()
    {
        scope a = "first";
        scope b = "second";
        string[] arr = [a, b];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    test中,不会编译初化arr.因为如果需要,语言会在初化时自动加scope到变量中,这令人惊讶.

    但是,请考虑scope string[] arr上的scope保护什么.可保护两样:数组中串地址,或串中字符地址.为了该赋值安全,scope必须保护串中字符,但它只保护顶级引用,即数组中串.因此,该示例不工作.现在更改arr为静态数组:

    @safe void test()
    {
        scope a = "first";
        scope b = "second";
        string[2] arr = [a, b];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为静态数组不是引用,这工作.在栈上就地分配所有元素内存(即,栈包含元素),与包含引用存储在其他地方元素动态数组不一样.
    静态数组为scope时,按scope对待元素.且如果arr,则该示例无法编译,因此可推导scope.

    实用技巧

    需要时间来理解DIP1000规则,许多人不愿学习.第一个也是最重要的技巧是:尽量避免非@safe代码.

    当然,该建议并不新鲜,但对DIP1000来说更重要.总之,在非@safe函数中,语言不会检查非安全函数的中域有效性,而调用这些函数时,编译器会假定满足了这些属性.

    因而在不安全代码中,中域危险.但是只要避免标记为@trusted,D代码几乎不会造成损害.@safe代码中滥用DIP1000可能会导致不必要编译错误,但它不会损坏内存和其他错误.

    值得一提的第二个重点是,如果函数属性仅接收静态GC分配数据,则不必用中域.
    许多语言禁止程序员引用栈.D可以这样,不代表必须用栈.这样,像DIP1000出来之前,不必花费更多时间来解决编译器错误.如果想用栈,就注解函数.这样,不破坏接口.

    下一步?

    本文,说明了在DIP1000中使用数组和指针.原则上,还允许读者用类和接口使用DIP1000.
    唯一要了解的是包括成员函数中this指针的类引用,同DIP1000一起就像指针一样使用.

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

  • 相关阅读:
    React + TypeScript 组件的状态和构造 React Hooks
    《golang设计模式》第三部分·行为型模式-06-备忘录模式(Memento)
    Java多线程——Callable和future
    用hadoop-eclipse-plugins-2.6.0来配置hadoop-3.3.6
    FFmpeg开发笔记(二十三)使用OBS Studio开启RTMP直播推流
    JAVA计算机毕业设计高校设备采购审批管理系统Mybatis+源码+数据库+lw文档+系统+调试部署
    MASM-环境搭建篇
    外汇危机使生产成本提高30%以上
    docker
    从Mpx资源构建优化看splitChunks代码分割
  • 原文地址:https://blog.csdn.net/fqbqrr/article/details/125409915