Enhancements can enable memory-safe reference counting

tsbockman thomas.bockman at gmail.com
Fri May 14 00:45:09 UTC 2021


I have written an experimental D reference counting system with a 
memory `@safe` API. It supports slices, classes, dynamic casts, 
and `-betterC`.

I found this task just barely possible today, with DIP25 / 
DIP1000 enabled (ignoring some bugs like [20150 "-dip1000 
defeated by 
pure"](https://issues.dlang.org/show_bug.cgi?id=20150)). But, 
some of the techniques I used to do it are rather nasty hacks, 
and I'm sure no one would want my solution in the standard 
library.

However, there are some fairly simple language enhancements that 
would make it possible to remove most of the hacks:

The most important changes relate to `scope` and `return` (as in 
DIP25's `return ref`):

(0) `return` should apply consistently to all indirections, 
especially `ref`, `*`, `[]`, and `class`, since these all use 
pointers under the hood.

(1) When calling a `return` annotated function, assigning the 
returned indirection should be considered valid or invalid based 
on whether the receiving indirection provably has a lifetime that 
is fully contained inside the lifetime of the `return` annotated 
input indirection(s) for the function.

(2) `foreach` and `foreach_reverse` should support `scope`.

Basically, annotating a function with `return` becomes a way to 
force callers to treat the return value as head `scope`, except 
when this can be proven unnecessary based on the lifetime of the 
relevant input(s).

A small example program (my real system is too large to embed in 
this message):
```D
module app;

import core.stdc.stdlib : malloc, free;
import core.lifetime : emplace;

import std.traits;

struct Unique(_Address)
     if(is(Unqual!_Address : Target*, Target) || is(_Address == 
class))
{
     alias Address = _Address;
     private Address _address;
     Address address() return pure @safe {
         return _address; }
     alias address this;

     static if(is(Unqual!Address : Target*, Target)) {
         alias Access = Target;
         ref Access access() return pure @safe {
             return *_address; }
         alias Slice = Access[];
         Access[] slice() return pure @trusted {
             return _address[0 .. (_address !is null)]; }
     } else {
	static assert(is(Address == class));
         alias Access = Address;
         Access access() return pure @safe {
             return _address; }
         alias Slice = const(Access)[];
         Slice slice() return const pure @trusted {
             return (*cast(Access[1]*) &_address)
                 [0 .. (_address !is null)];
         }
     }
     alias opUnary(string op : `*`) = access;
     alias opIndex() = slice;

     this(const(bool) value) @trusted {
         if(value) {
             static if(is(Access == Address)) {
                 _address = cast(Address)
                 	malloc(__traits(classInstanceSize, Access));
             } else {
         		_address = cast(Address)
                 	malloc(Access.sizeof);
             }
             emplace(_address);
         } else
             _address = null;
     }
     @disable ref typeof(this) opAssign(ref typeof(this));
     @disable this(this);
     @disable this(ref typeof(this));
     ~this() @trusted {
         if(_address !is null) {
             destroy!false(access);
         	free(cast(void*) _address);
             _address = null;
         }
     }
}

void test(Address)() @safe {
     Unique!Address up = true;
     with(up) {
         static a = Address.init, c = Address.init, e = Slice.init;
         scope b = Address.init, d = Address.init, f = Slice.init;

         static if( __traits(compiles, a = up.address)) {
             pragma(msg, "a: ACCEPTS INVALID: static " ~ 
Address.stringof ~
                 " = return " ~ Address.stringof);
         }
         static if(!__traits(compiles, b = up.address)) {
             pragma(msg, "d: REJECTS VALID: scope " ~ 
Address.stringof ~
				" = return " ~ Address.stringof);
         }
         static if(!is(Access == Address)) {
             static if( __traits(compiles, c = &(up.access))) {
                 pragma(msg, "b: ACCEPTS INVALID: static " ~ 
Address.stringof ~
                 	" = &(return ref " ~ Access.stringof ~ ")");
             }
             static if(!__traits(compiles, d = &(up.access))) {
                 pragma(msg, "e: REJECTS VALID: scope " ~ 
Address.stringof ~
					" = &(return ref " ~ Access.stringof ~ ")");
             }
         }
         static if( __traits(compiles, e = up.slice)) {
             pragma(msg, "c: ACCEPTS INVALID: static " ~ 
Slice.stringof ~
				" = return " ~ Slice.stringof);
         }
         static if(!__traits(compiles, f = up.slice)) {
             pragma(msg, "f: REJECTS VALID: scope " ~ 
Slice.stringof ~
				" = return " ~ Slice.stringof);
         }

         static if(!is(Access == Address)) {
             static Access g, i, k;
             scope Access h, j, l;

             static if(!__traits(compiles, g = up.access)) {
                 pragma(msg, "g: REJECTS VALID: static " ~ 
Access.stringof ~
					" = return ref " ~ Access.stringof);
             }
             static if(!__traits(compiles, h = up.access)) {
                 pragma(msg, "j: REJECTS VALID: scope " ~ 
Access.stringof ~
					" = return ref " ~ Access.stringof);
             }
             static if(!__traits(compiles, i = *(up.address))) {
                 pragma(msg, "h: REJECTS VALID: static " ~ 
Access.stringof ~
					" = *(return " ~ Address.stringof ~ ")");
             }
             static if(!__traits(compiles, j = *(up.address))) {
                 pragma(msg, "k: REJECTS VALID: scope " ~ 
Access.stringof ~
					" = *(return " ~ Address.stringof ~ ")");
             }
             static if(!__traits(compiles, k = up.slice[0])) {
                 pragma(msg, "i: REJECTS VALID: static " ~ 
Access.stringof ~
					" = (return " ~ Slice.stringof ~ ")[0]");
             }
             static if(!__traits(compiles, l = up.slice[0])) {
                 pragma(msg, "l: REJECTS VALID: scope " ~ 
Access.stringof ~
					" = (return " ~ Slice.stringof ~ ")[0]");
             }
         }
     }
}

class D { }
void main() @safe {
     test!(int**)();
     test!D();
}
```
Output with `-dip1000`:
```
a: ACCEPTS INVALID: static int** = return int**
e: REJECTS VALID: scope int** = &(return ref int*)
c: ACCEPTS INVALID: static int*[] = return int*[]
a: ACCEPTS INVALID: static D = return D
```
(`-dip1000` only prevented this one error, although I believe it 
does do some other good things that are not tested above):
```
c: ACCEPTS INVALID: static const(D)[] = return const(D)[]
```

Currently, in order to have a truly `@safe` API I must work 
around the above issues by marking various things `@system` that 
shouldn't need to be `@system`, and then offering awkward but 
safe borrowing with something like this:
```D
mixin template borrow(alias owner, string name) {
     mixin(`scope `, name, ` = () @trusted { pragma(inline, true); 
return owner.address; }();`);
}
```
With my earlier proposed changes to `return` and `scope`, though, 
the `borrow` mixin would be unnecessary.

I think D is very close to being able to sanely express `@safe` 
reference counting APIs. I don't think `@live` is necessary; 
rather, we just need to complete `scope` and `return` and fix 
some RAII related bugs. For performance reasons, move operators 
and some minor changes to the GC would also be good, but are not 
actually required.

Destroy?


More information about the Digitalmars-d mailing list