-
-
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); | |
} |
@radcapricorn I get an assertion failure on the line auto m = move(n);
. Seems like the borrow checking is working as intended here.
Hm, that probably would be after that line, on destruction of m. But anyway, I don't even get there, as in the code as presented, borrow
is @System.
You need to compile with -preview=dip1000
to make borrow
@safe
.
Here's a different example that I think illustrates the issue better:
@safe void main()
{
Owned!int n = getOwned();
Borrowed!int b1 = n.borrow;
Owned!int m = move(n);
destroy(b1); // still points to n; borrowCount == -1
Borrowed!int b2 = n.borrow; // borrowCount == 0
destroy(n); // oops
b2.apply!writeln; // null dereference (not UB)
}
So far I have not come up with a way to get actual UB out of this.
I am :) Also, try putting, say, a writeln(x)
after the move. Here I get infinite assert loop.
Put an explicit `@safe` on `borrow`:
$ dmd -preview=dip1000 -i app.d
borrowcheck.d(54): Error: reference to local variable `owner` assigned to non-scope `Borrowed(null)`
app.d(21): Error: template instance `borrowcheck.borrow!int` error instantiating
Huh, looks like it works on DMD 2.097 (the version on run.dlang.io) but fails on DMD 2.098. I guess if DIP 1000 can't handle this kind of ref
→ pointer conversion, that pretty much sinks the whole idea.
Let's assume for the moment that could be a bug. I wanna stress test this more :)
You know, aside from that infinite assertion loop if you try to move it out in a lambda, this does look promising. Pending the borrow
implementation that's @safe
.
If you let the borrow be put in a variable you can just destroy it inside apply like this:
@safe void main()
{
Owned!int n = makeOwned(123);
Borrowed!int b = n.borrow;
b.apply!((ref x) {
destroy(b);
destroy(n);
Owned!int = makeOwned(456);
writeln(x); // prints 456
});
}
This prints 456
for me and the program ends with code -11
.
Good point. I guess this means that you can't really separate borrow
and apply
—you need apply
itself to increment and decrement the borrow count. Something like
auto apply(alias fun, T)(auto ref Owned!T this_)
{
this_.borrowCount++;
scope(exit) this_.borrowCount--;
scope ptr = this_.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!
}
No references escaped, but you do get memory corruption.