1 /// Reference counting module
2 module gfx.core.rc;
3
4 import gfx.core.log : LogTag;
5 import std.typecons : Flag, No, Yes;
6
7 enum gfxRcMask = 0x4000_0000;
8 immutable gfxRcLog = LogTag("GFX-RC", gfxRcMask);
9
10 /// A resource that can be disposed
11 interface Disposable
12 {
13 /// Dispose the underlying resource
14 void dispose();
15 }
16
17 /// A atomic reference counted resource.
18 /// Objects implementing this interface can be safely manipulated as shared.
19 /// Implementing class should mixin atomicRcCode to use the provided implementation.
20 interface IAtomicRefCounted
21 {
22 /// Atomically loads the number of active references.
23 final @property size_t refCount() const {
24 return (cast(shared(IAtomicRefCounted))this).refCountShared;
25 }
26 /// ditto
27 shared @property size_t refCountShared() const;
28
29 /// Atomically increment the reference count.
30 final void retain() {
31 return (cast(shared(IAtomicRefCounted))this).retainShared();
32 }
33 /// ditto
34 void retainShared() shared;
35
36 /// Atomically decrement the reference count.
37 /// If refCount reaches zero, and disposeOnZero is set,
38 /// the object is locked with its own mutex, and dispose is called.
39 /// In most cases, the calling code should set disposeOnZero, unless it
40 /// is intended to release the object to give it away.
41 /// (such as at the end of a builder function)
42 /// Returns: true if the object was disposed during this call, false otherwise.
43 final bool release(in Flag!"disposeOnZero" disposeOnZero = Yes.disposeOnZero)
44 in {
45 assert(
46 refCount > 0,
47 "inconsistent ref count for "~(cast(Object)this).classinfo.name
48 );
49 }
50 body {
51 return (cast(shared(IAtomicRefCounted))this).releaseShared(disposeOnZero);
52 }
53 /// ditto
54 bool releaseShared(in Flag!"disposeOnZero" disposeOnZero = Yes.disposeOnZero) shared
55 in {
56 assert(
57 refCountShared > 0,
58 "inconsistent ref count for "~(cast(Object)this).classinfo.name
59 );
60 }
61
62 /// Returns whether the refCount >= 1.
63 /// This increases the refCount by 1. rcLock should be used to keep
64 /// weak reference and ensures that the resource is not disposed.
65 /// The operation is atomic.
66 final bool rcLock()
67 out(res) {
68 assert(
69 (res && refCount >= 2) || (!res && !refCount),
70 "inconsistent ref count for "~(cast(Object)this).classinfo.name
71 );
72 }
73 body {
74 return (cast(shared(IAtomicRefCounted))this).rcLockShared();
75 }
76 /// ditto
77 shared bool rcLockShared();
78 // out(res) {
79 // assert(
80 // (res && refCountShared >= 2) || (!res && !refCountShared),
81 // "inconsistent ref count for "~(cast(Object)this).classinfo.name
82 // );
83 // }
84 // this contract compiles with dmd but create a failure on ldc2:
85 // cannot implicitely convert shared(T) to const(IAtomicRefCounted)
86
87 /// Dispose the underlying resource
88 void dispose()
89 in { assert(refCount == 0); }
90 }
91
92 /// Abstract class that implements IAtomicRefCounted.
93 /// Should be used over IAtomicRefCounted when practicable to avoid code
94 /// duplication in the final executable.
95 abstract class AtomicRefCounted : IAtomicRefCounted
96 {
97 mixin(atomicRcCode);
98
99 abstract override void dispose();
100 }
101
102 /// compile time check that T can be ref counted atomically.
103 enum isAtomicRefCounted(T) = is(T : shared(IAtomicRefCounted)) || is(T : IAtomicRefCounted);
104
105
106 /// A string that can be mixed-in a class declaration to implement IAtomicRefCounted.
107 /// dispose is not implemented of course, but is called by release while the object is locked.
108 /// Classes implementing it are free to do it in a non-thread safe manner as long
109 /// as dispose does not manipulate external state.
110 enum atomicRcCode = sharedAtomicMethods ~ q{
111 private size_t _refCount=0;
112 };
113
114 /// Counts the number of references of a single object.
115 size_t countObj(T)(T obj) if (isAtomicRefCounted!T)
116 {
117 static if (is(T == shared)) {
118 return obj.refCountShared;
119 }
120 else {
121 return obj.refCount;
122 }
123 }
124
125 /// Retains a single object.
126 T retainObj(T)(T obj) if (isAtomicRefCounted!T)
127 {
128 static if (is(T == shared)) {
129 obj.retainShared();
130 }
131 else {
132 obj.retain();
133 }
134 return obj;
135 }
136
137 /// Releases and nullify a single object. The object may be null.
138 /// Returns: whether the object was disposed.
139 bool releaseObj(T)(ref T obj, in Flag!"disposeOnZero" disposeOnZero = Yes.disposeOnZero)
140 if (isAtomicRefCounted!T)
141 {
142 if (!obj) return false;
143
144 static if (is(T == shared)) {
145 const res = obj.releaseShared(disposeOnZero);
146 obj = null;
147 return res;
148 }
149 else {
150 const res = obj.release(disposeOnZero);
151 obj = null;
152 return res;
153 }
154 }
155
156 /// Locks a single object.
157 T lockObj(T)(T obj) if (isAtomicRefCounted!T)
158 in {
159 assert(obj, "locking null object");
160 }
161 body {
162 static if (is(T == shared)) {
163 if (obj.rcLockShared()) {
164 return obj;
165 }
166 }
167 else {
168 if (obj.rcLock()) {
169 return obj;
170 }
171 }
172 return null;
173 }
174
175 /// Decreases the reference count of a single object without disposing it.
176 /// Use this to move an object out of a scope (typically return at the end of a function)
177 T giveAwayObj(T)(ref T obj) if (is(T : IAtomicRefCounted))
178 in {
179 assert(obj, "giving away null object");
180 }
181 body {
182 auto o = obj;
183 releaseObj(obj, No.disposeOnZero);
184 return o;
185 }
186
187 /// Dispose and nullify a single object, that might be null
188 void disposeObj(T)(ref T obj) if (is(T : Disposable))
189 {
190 if (obj) {
191 obj.dispose();
192 obj = null;
193 }
194 }
195
196 /// Retain GC allocated array of ref-counted resources
197 void retainArr(T)(ref T[] arr) if (isAtomicRefCounted!T)
198 {
199 import std.algorithm : each;
200 arr.each!(el => retainObj(el));
201 }
202
203 /// Release GC allocated array of ref-counted resources
204 void releaseArr(T)(ref T[] arr) if (isAtomicRefCounted!T)
205 {
206 import std.algorithm : each;
207 arr.each!(el => releaseObj(el));
208 arr = null;
209 }
210
211 /// Dispose GC allocated array of resources
212 void disposeArr(T)(ref T[] arr) if (is(T : Disposable))
213 {
214 import std.algorithm : each;
215 arr.each!(el => el.dispose());
216 arr = null;
217 }
218
219 /// Retain GC allocated associative array of ref-counted resources
220 void retainAA(T, K)(ref T[K] arr) if (isAtomicRefCounted!T)
221 {
222 import std.algorithm : each;
223 arr.each!((k, el) => retainObj(el));
224 }
225 /// Release GC allocated associative array of ref-counted resources
226 void releaseAA(T, K)(ref T[K] arr) if (isAtomicRefCounted!T)
227 {
228 import std.algorithm : each;
229 arr.each!((k, el) => releaseObj(el));
230 arr.clear();
231 arr = null;
232 }
233
234 /// Dispose GC allocated associative array of resources
235 void disposeAA(T, K)(ref T[K] arr) if (is(T : Disposable))
236 {
237 import std.algorithm : each;
238 arr.each!((k, el) { el.dispose(); });
239 arr = null;
240 }
241
242 /// Reinitialises a single struct
243 /// Useful if the struct release resource in its destructor.
244 void reinitStruct(T)(ref T t) if (is(T == struct))
245 {
246 t = T.init;
247 }
248
249 /// Reinitialises a GC allocated array of struct.
250 /// Useful if the struct release resource in its destructor.
251 void reinitArr(T)(ref T[] arr) if (is(T == struct))
252 {
253 foreach(ref t; arr) {
254 t = T.init;
255 }
256 arr = null;
257 }
258 /// Reinitialises a GC allocated associative array of struct.
259 /// Useful if the struct release resource in its destructor.
260 void reinitAA(T, K)(ref T[K] arr) if (is(T == struct))
261 {
262 foreach(k, ref t; arr) {
263 t = T.init;
264 }
265 arr.clear();
266 arr = null;
267 }
268
269 /// Helper that build a new instance of T and returns it within a Rc!T
270 template makeRc(T) if (isAtomicRefCounted!T)
271 {
272 Rc!T makeRc(Args...)(Args args)
273 {
274 return Rc!T(new T(args));
275 }
276 }
277
278 /// Helper that places an instance of T within a Rc!T
279 template rc(T) if (isAtomicRefCounted!T)
280 {
281 Rc!T rc(T obj)
282 {
283 if (obj) {
284 return Rc!T(obj);
285 }
286 else {
287 return (Rc!T).init;
288 }
289 }
290 }
291
292 /// Produces an Rc!T holding a null object
293 @property Rc!T nullRc(T)() if (isAtomicRefCounted!T) {
294 return Rc!T.init;
295 }
296
297 /// Helper struct that manages the reference count of an object using RAII.
298 struct Rc(T) if (isAtomicRefCounted!T)
299 {
300 private T _obj;
301
302 /// Build a Rc instance with the provided resource
303 this(T obj)
304 {
305 _obj = obj;
306 if (obj) {
307 retainObj(_obj);
308 }
309 }
310
311 /// Postblit adds a reference to the held reference.
312 this(this)
313 {
314 if (_obj) retainObj(_obj);
315 }
316
317 /// Removes a reference to the held reference.
318 ~this()
319 {
320 if(_obj) {
321 debug {
322 import core.exception : InvalidMemoryOperationError;
323
324 try {
325 releaseObj(_obj, Yes.disposeOnZero);
326 }
327 catch(InvalidMemoryOperationError error)
328 {
329 import core.stdc.stdio : stderr, printf;
330
331 enum fmtString = "InvalidMemoryOperationError thrown when releasing %s."
332 ~ " This is almost certainly due to release during garbage"
333 ~ " collection through field destructor because the object"
334 ~ " was not properly released before.\n";
335
336 printf(fmtString, T.stringof.ptr);
337
338 throw error;
339 }
340 }
341 else {
342 releaseObj(_obj, Yes.disposeOnZero);
343 }
344 _obj = null;
345 }
346 }
347
348 /// Assign another resource. Release the previously held ref and retain the new one.
349 void opAssign(T obj)
350 {
351 if (obj is _obj) return;
352
353 if(_obj) releaseObj(_obj, Yes.disposeOnZero);
354 _obj = obj;
355 if(_obj) retainObj(_obj);
356 }
357
358 /// Check whether this Rc is assigned to a resource.
359 bool opCast(T : bool)() const
360 {
361 return loaded;
362 }
363
364 /// Check whether this Rc is assigned to a resource.
365 @property bool loaded() const
366 {
367 return _obj !is null;
368 }
369
370 /// Reset the resource.
371 void unload()
372 {
373 if(_obj) {
374 releaseObj(_obj);
375 }
376 }
377
378 /// Decrease the ref count and return the object without calling dispose
379 T giveAway()
380 in {
381 assert(_obj, "giving away invalid object");
382 }
383 out(obj) {
384 assert(obj);
385 }
386 body {
387 auto obj = _obj;
388 releaseObj(_obj, No.disposeOnZero);
389 return obj;
390 }
391
392 /// Access to the held resource.
393 @property inout(T) obj() inout { return _obj; }
394
395 alias obj this;
396 }
397
398 /// Helper struct that keeps a weak reference to a Resource.
399 struct Weak(T) if (is(T : IAtomicRefCounted))
400 {
401 private T _obj;
402
403 /// Build a Weak instance.
404 this(T obj)
405 {
406 _obj = obj;
407 }
408
409 /// Reset the internal reference.
410 void reset()
411 {
412 _obj = null;
413 }
414
415 /// Check whether the resource has been disposed.
416 @property bool disposed() const
417 {
418 return !_obj || (_obj.refCount == 0);
419 }
420
421 /// Return a Rc that contain the underlying resource if it has not been disposed.
422 Rc!T lock()
423 {
424 Rc!T rc;
425 rc._obj = _obj ? lockObj(_obj) : null;
426 if (!rc._obj) _obj = null;
427 return rc;
428 }
429 }
430
431 debug(rc) {
432 __gshared string rcTypeRegex = ".";
433 __gshared bool rcPrintStack = false;
434 }
435
436 private enum sharedAtomicMethods = q{
437
438 import std.typecons : Flag, Yes;
439 import std.traits : Unqual;
440
441 debug {
442 ~this() {
443 // no gc operation allowed during gc pass
444 import std.stdio : stderr;
445 enum phrase = rcTypeName ~ " was not properly disposed\n";
446 if (refCount != 0) {
447 stderr.write(phrase);
448 stderr.flush();
449 }
450 }
451 enum rcTypeName = Unqual!(typeof(this)).stringof;
452 }
453
454 debug(rc) {
455 private final shared void rcDebug(Args...)(string fmt, Args args)
456 {
457 import gfx.core.rc : gfxRcLog, rcPrintStack, rcTypeRegex;
458 import gfx.core.util : StackTrace;
459 import std.algorithm : min;
460 import std.regex : matchFirst;
461
462 if (!matchFirst(rcTypeName, rcTypeRegex)) return;
463
464 gfxRcLog.debugf(fmt, args);
465 if (rcPrintStack) {
466 const st = StackTrace.obtain(13, StackTrace.Options.all);
467 const frames = st.frames.length > 2 ? st.frames[2 .. $] : [];
468 foreach (i, f; frames) {
469 gfxRcLog.debugf(" %02d %s", i, f.symbol);
470 }
471 }
472 }
473 }
474
475
476 public final shared override @property size_t refCountShared() const
477 {
478 import core.atomic : atomicLoad;
479 return atomicLoad(_refCount);
480 }
481
482 public final shared override void retainShared()
483 {
484 import core.atomic : atomicOp;
485 const rc = atomicOp!"+="(_refCount, 1);
486 debug(rc) {
487 rcDebug("retain %s: %s -> %s", rcTypeName, rc-1, rc);
488 }
489 }
490
491 public final shared override bool releaseShared(in Flag!"disposeOnZero" disposeOnZero = Yes.disposeOnZero)
492 {
493 import core.atomic : atomicOp;
494 const rc = atomicOp!"-="(_refCount, 1);
495
496 debug(rc) {
497 rcDebug("release %s: %s -> %s", rcTypeName, rc+1, rc);
498 }
499 if (rc == 0 && disposeOnZero) {
500 debug(rc) {
501 rcDebug("dispose %s", rcTypeName);
502 }
503 synchronized(this) {
504 // cast shared away
505 auto obj = cast(Unqual!(typeof(this)))this;
506 obj.dispose();
507 }
508 return true;
509 }
510 else {
511 return false;
512 }
513 }
514
515 public final shared override bool rcLockShared()
516 {
517 import core.atomic : atomicLoad, cas;
518 while (1) {
519 const c = atomicLoad(_refCount);
520
521 if (c == 0) {
522 debug(rc) {
523 rcDebug("rcLock %s: %s", rcTypeName, c);
524 }
525 return false;
526 }
527 if (cas(&_refCount, c, c+1)) {
528 debug(rc) {
529 rcDebug("rcLock %s: %s", rcTypeName, c+1);
530 }
531 return true;
532 }
533 }
534 }
535 };
536
537 // private string numberizeCodeLines(string code) {
538 // import std.string : splitLines;
539 // import std.format : format;
540 // string res;
541 // auto lines = code.splitLines();
542 // foreach (i, l; lines) {
543 // res ~= format("%s. %s\n", i+1, l);
544 // }
545 // return res;
546 // }
547
548 // pragma(msg, numberizeCodeLines(atomicRcCode));
549
550 // using version(unittest) instead of private creates link errors
551 // in test builds in apps/libs depending on gfx-d (??)
552 private
553 {
554 int rcCount = 0;
555 int structCount = 0;
556
557 class RcClass : IAtomicRefCounted
558 {
559 mixin(atomicRcCode);
560
561 this()
562 {
563 rcCount += 1;
564 }
565
566 override void dispose()
567 {
568 rcCount -= 1;
569 }
570 }
571
572 struct RcStruct
573 {
574 Rc!RcClass obj;
575 }
576
577 struct RcArrStruct
578 {
579 Rc!RcClass[] objs;
580
581 ~this()
582 {
583 foreach(ref o; objs) {
584 o = Rc!RcClass.init;
585 }
586 }
587 }
588
589 struct RcArrIndStruct
590 {
591 RcStruct[] objs;
592
593 ~this()
594 {
595 foreach(ref o; objs) {
596 o = RcStruct.init;
597 }
598 }
599 }
600
601
602 unittest
603 {
604 {
605 auto arr = RcArrStruct([makeRc!RcClass(), makeRc!RcClass()]);
606 assert(rcCount == 2);
607 foreach(obj; arr.objs) {
608 assert(rcCount == 2);
609 }
610 assert(rcCount == 2);
611 }
612 assert(rcCount == 0);
613 }
614
615
616 unittest
617 {
618 {
619 auto obj = makeRc!RcClass();
620 assert(rcCount == 1);
621 }
622 assert(rcCount == 0);
623 }
624
625 unittest
626 {
627 {
628 auto obj = RcStruct(makeRc!RcClass());
629 assert(rcCount == 1);
630 }
631 assert(rcCount == 0);
632 }
633
634 unittest
635 {
636 Weak!RcClass weak;
637 {
638 auto locked = weak.lock();
639 assert(!locked.loaded);
640 }
641 {
642 auto rc = makeRc!RcClass();
643 assert(rcCount == 1);
644 assert(rc.refCount == 1);
645 weak = Weak!RcClass(rc);
646 assert(rc.refCount == 1);
647 {
648 auto locked = weak.lock();
649 assert(locked.loaded);
650 assert(rc.refCount == 2);
651 }
652 assert(rc.refCount == 1);
653 }
654 assert(rcCount == 0);
655 assert(weak.disposed);
656 {
657 auto locked = weak.lock();
658 assert(!locked.loaded);
659 }
660 }
661 }