Skip to content

Commit 2ded351

Browse files
Araqryanwalklin
andauthored
lifter: destroy an inheritable object through its vtable, even an emp… (#2532)
…ty one A closure's environment is a ref to a compiler-made object deriving from RootObj, and the closure value knows it only as `(ref RootObj)`. Its =destroy decremented the count and freed the block, but never ran the env's OWN destructor: RootObj has no fields, `isTrivialTypeDecl` called it trivial, and `emitRefDestructor` skipped the payload. Every captured string, ref or resource in every closure leaked. An object with RTTI is never trivial for `=destroy` now: the value may be a DERIVED object that owns something, and its `=destroy` is a method. The synthesized `=destroy_RootObj` is an empty method — the vtable slot is the point — and the `ref RootObj` hook dispatches through it to the env's synthesized destroy, which releases the captures. Same mechanism the existing user-hierarchy dispatch (txdestroy) relies on; `=wasMoved` already had this exception for the vtable field. Test: tclosure_env_release — a captured resource with a loud destructor is released when the last reference to the closure goes, and not before. Co-authored-by: Ryan Walklin <ryan@kaitakeradiology.co.nz>
1 parent 005e934 commit 2ded351

4 files changed

Lines changed: 78 additions & 3 deletions

File tree

src/hexer/lifter.nim

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,21 @@ proc isTrivialTypeDecl(c: var LiftingCtx; n: Cursor): bool =
236236
result = false
237237
of ObjectT:
238238
result = isTrivialObjectBody(c, r.body)
239-
if result and c.op == attachedWasMoved and hasRtti(r.pragmas):
240-
# We set the RTTI field in `=wasMoved` so objects with a vtable are not trivial.
241-
result = false
239+
if result and hasRtti(r.pragmas):
240+
if c.op == attachedWasMoved:
241+
# We set the RTTI field in `=wasMoved` so objects with a vtable are not trivial.
242+
result = false
243+
elif c.op == attachedDestroy:
244+
# A value of an inheritable type may really be a DERIVED object that
245+
# owns resources, and its `=destroy` is a method: the destroy has to
246+
# be dispatched through the vtable even when this class itself has
247+
# nothing to release. `RootObj` is the case that matters: a closure's
248+
# environment is `(ref RootObj)` to everyone but its maker, and
249+
# treating `RootObj` as trivial freed the env block without ever
250+
# running the env's own destructor — every captured string, ref or
251+
# resource leaked. The synthesized `=destroy_RootObj` is an empty
252+
# method; the vtable slot is the point.
253+
result = false
242254
of DistinctT:
243255
# A `distinct T` with no hooks of its own inherits the triviality of its
244256
# base type: `distinct string` is non-trivial (it owns the same heap
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# The foreign half of tclosure_env_release: a resource with a loud destructor,
2+
# and an object holding a closure in a field of the semchecked (never lowered
3+
# here) `.closure` type.
4+
{.feature: "lenientnils".}
5+
import std/syncio
6+
7+
type
8+
Resource* = object
9+
name*: string
10+
11+
proc `=destroy`*(r: Resource) =
12+
if r.name.len > 0: echo "released ", r.name
13+
14+
type
15+
Holder* = object
16+
onEvent*: proc() {.closure.}
17+
18+
proc clear*(h: var Holder) = h.onEvent = nil
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# A closure's environment must release what it captured when the last
2+
# reference to the closure goes away. The env is a `ref` to a compiler-made
3+
# object deriving from RootObj, and the closure value only knows it as
4+
# `(ref RootObj)`: its `=destroy` decremented the count and freed the block,
5+
# but never ran the env's OWN destructor (RootObj has no fields, so the
6+
# payload counted as trivial) — every captured string, ref or resource in
7+
# every closure leaked. `=destroy(RootObj)` is a (empty) virtual method now,
8+
# so the `ref RootObj` hook dispatches through the env's vtable.
9+
{.feature: "lenientnils".}
10+
import std/syncio
11+
import deps/mclosureenvleak
12+
13+
proc touch(what: string; r: Resource) = echo what, r.name
14+
15+
proc fire(h: Holder) =
16+
# its own scope: the temp the call takes on the closure value dies here,
17+
# not at the end of the caller
18+
h.onEvent()
19+
20+
proc attach(h: var Holder; tag: string) =
21+
var res = Resource(name: tag)
22+
let cb = proc() {.closure.} = touch("event ", res)
23+
h.onEvent = cb
24+
25+
proc main() =
26+
var h = Holder()
27+
attach(h, "alpha")
28+
fire(h)
29+
clear(h) # last reference: the env goes, and with it "alpha"
30+
echo "cleared"
31+
attach(h, "beta")
32+
let copy = h.onEvent # a second reference
33+
clear(h)
34+
copy() # still alive through `copy`
35+
echo "scope end next"
36+
37+
main()
38+
echo "ok"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
event alpha
2+
released alpha
3+
cleared
4+
event beta
5+
scope end next
6+
released beta
7+
ok

0 commit comments

Comments
 (0)