Skip to content

Instantly share code, notes, and snippets.

@run-dlang
Created November 9, 2021 16:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save run-dlang/d1982a29423b2cb545bc9fa452d94c5e to your computer and use it in GitHub Desktop.
Save run-dlang/d1982a29423b2cb545bc9fa452d94c5e to your computer and use it in GitHub Desktop.
Code shared from run.dlang.io. Run with '-preview=dip1000'
--- 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
Copy link

radcapricorn commented Nov 9, 2021

@safe void main()
{
    Owned!int n = getOwned();
    // can access and modify value
    n.borrow.apply!writeln;
    n.borrow.apply!((ref x) {
        x = 456;
        auto m = move(n);
    });
    n.borrow.apply!writeln;
}

No references escaped, but you do get memory corruption.

@pbackus
Copy link

pbackus commented Nov 9, 2021

@radcapricorn I get an assertion failure on the line auto m = move(n);. Seems like the borrow checking is working as intended here.

@radcapricorn
Copy link

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.

@pbackus
Copy link

pbackus commented Nov 9, 2021

You need to compile with -preview=dip1000 to make borrow @safe.

@pbackus
Copy link

pbackus commented Nov 9, 2021

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.

@radcapricorn
Copy link

I am :) Also, try putting, say, a writeln(x) after the move. Here I get infinite assert loop.

@radcapricorn
Copy link

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

@pbackus
Copy link

pbackus commented Nov 9, 2021

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.

@radcapricorn
Copy link

Let's assume for the moment that could be a bug. I wanna stress test this more :)

@radcapricorn
Copy link

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.

@victor-carvalho
Copy link

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.

@pbackus
Copy link

pbackus commented Nov 9, 2021

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);
}

@victor-carvalho
Copy link

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.

@victor-carvalho
Copy link

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 🙂

@victor-carvalho
Copy link

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.

@pbackus
Copy link

pbackus commented Nov 9, 2021

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.

@tsbockman
Copy link

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!
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment