-
-
Save run-dlang/d1982a29423b2cb545bc9fa452d94c5e to your computer and use it in GitHub Desktop.
--- app.d | |
import borrowcheck; | |
import std.stdio; | |
import core.lifetime; | |
@safe Owned!int getOwned() | |
{ | |
Owned!int n = makeOwned(123); | |
// can return by move (or NRVO) | |
return move(n); | |
} | |
@safe void receiveOwned(Owned!int n) | |
{ | |
return; | |
} | |
@safe void main() | |
{ | |
Owned!int n = getOwned(); | |
// can access and modify value | |
n.borrow.apply!writeln; | |
n.borrow.apply!((ref n) { n = 456; }); | |
n.borrow.apply!writeln; | |
// can't escape a reference: | |
//int* p = n.borrow.apply!((return ref n) => &n); | |
// can't escape a borrow: | |
static Borrowed!int global; | |
//global = n.borrow; | |
// can't destroy while borrowed | |
{ | |
Borrowed!int b = n.borrow; | |
//destroy(n); // assertion failure | |
} | |
// can pass by move | |
receiveOwned(move(n)); | |
// no double free | |
assert(n == Owned!int.init); | |
} | |
--- borrowcheck.d | |
import core.stdc.stdlib; | |
struct Owned(T) | |
{ | |
private T* ptr; | |
private int borrowCount; | |
// ptr must be allocated with malloc | |
@system this(T* ptr) | |
{ | |
this.ptr = ptr; | |
} | |
~this() | |
{ | |
assert(borrowCount == 0); | |
// ok because borrowCount == 0 guarantees exclusive access | |
() @trusted { free(ptr); }(); | |
} | |
@disable this(ref inout typeof(this) other) inout; | |
} | |
Owned!T makeOwned(T)(T value) | |
{ | |
// ok because malloc has a safe interface | |
void* rawPtr = (() @trusted => malloc(T.sizeof))(); | |
// ok because we only use this memory to hold a T | |
T* ptr = (() @trusted => cast(T*) rawPtr)(); | |
*ptr = value; | |
// ok because ptr is allocated with malloc | |
return (() @trusted => Owned!T(ptr))(); | |
} | |
struct Borrowed(T) | |
{ | |
Owned!T* owner; | |
this(Owned!T* owner) | |
{ | |
this.owner = owner; | |
assert(owner.borrowCount < int.max, "Too many borrows!"); | |
owner.borrowCount++; | |
} | |
~this() | |
{ | |
owner.borrowCount--; | |
} | |
} | |
Borrowed!T borrow(T)(return ref Owned!T owner) | |
{ | |
return Borrowed!T(&owner); | |
} | |
auto apply(alias fun, T)(auto ref Borrowed!T this_) | |
{ | |
// ensure no reference to *ptr can escape from `apply` | |
scope T* ptr = this_.owner.ptr; | |
return fun(*ptr); | |
} |
I think this way it can be made @safe
(at least I couldn't find any way to break it) but it is not very practical to use if every access has to go through a lambda. It would be a callback hell to work with multiple instances like this.
I will play a little more with this to see if I can find any other problem.
It could be made to work with foreach too but it would be better if foreach supported multiple statement like Scala's for
.
Or we could add a keyword like using
from C# that we could use like this:
using borrow1 = owned1;
using borrow2 = owned2;
Just dreaming here a little here 🙂
Just to clarify, I know we could write it like this:
foreach(ref x; owned1)
foreach(ref y; owned2)
{
// code here
}
But it's not pretty.
Yes, it's definitely not ideal. Some kind of syntax sugar for monads, like Scala's for
or Haskell's do
, would help a lot with this sort of thing.
I think your general approach here may be sound (but awkward to use).
However, the implementation does have some problems:
@safe void main()
{
Owned!int n = getOwned();
Borrowed!int b = &n;
Borrowed!int c;
c.owner = &n; // Borrowed.owner should be private.
// There are @safe ways of calling destructors manually which do not set the target to .init afterward.
// So, destructors should explicitly set pointer members to null:
c.__dtor();
destroy!false(n);
// Use after free:
c.apply!writeln;
b.apply!((ref n) { n = 1; });
c.apply!writeln;
// Constructors can be manually called in @safe code, too:
b.__ctor(&n);
c.__ctor(&n);
// No assertions failed in the making of this undefined behavior!
}
Good point. I guess this means that you can't really separate
borrow
andapply
—you needapply
itself to increment and decrement the borrow count. Something like