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 }