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"). 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):
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:
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?