On Wednesday, 22 September 2021 at 19:30:37 UTC, Paul Backus wrote:
> Either (a) the compiler must assume, pessimistically, that a pointer passed to a function may be stored somewhere the GC can't see it, and therefore must be preserved in local storage until the end of its lexical lifetime, or (b) the user must manually signal to the compiler or the GC that the pointer should be kept alive before passing it to the function.
Given how error-prone option (b) is, I think (a) is the more sensible choice. Users who don't want the pointer kept alive can always opt-in to that behavior by enclosing it in a block scope to limit its lifetime, or setting it to null
when they're done with it.
Neither of these solutions would've helped with the original code that used the epoll API.
The pointer passed to C wasn't important, and it would've been fine for its lifetime to end at the end of the function (it was even a pointer to a stack-allocated struct in the function: epoll copies the struct out of the pointer given to it).
The pointer that mattered was a misaligned class reference in the structure passed to C. This pointer was to an object that will eventually get destructed before the end of its function scope. That objected wasn't passed anywhere in its scope, it was initialized and then had a method called on it.
At the site of the short-lived object, there's nothing apparently wrong. At the site of the C API call, what can you do? If you tell the GC that the short-lived object's reference is important, the GC still can't see any references to it afterward, and the GC isn't responsible for the object's short lifetime. If you tell the compiler that the short-lived object's reference is important, this could be a completely separate compilation and the decision to expire it early might've already been made.
I think the intuition that's violated here is "the GC is nondeterministic, but the stack is deterministic." And the correct intuition is "class destruction is nondeterministic, but struct destruction is (usually) deterministic."
This also can happen without using calling out to C:
import std.stdio : writeln;
import core.memory : GC;
struct Hidden {
align(1):
int spacer;
C obj;
}
Hidden hidden;
class C {
int id;
this(int id) {
this.id = id;
hidden.obj = this;
}
~this() {
id = -id;
writeln("dtor");
}
}
void check() {
writeln(hidden.obj.id);
}
void main() {
auto c = new C(17);
check; // 17, c is alive
GC.collect;
GC.collect;
check; // -17, c was destroyed
writeln("here");
}