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 interface AtomicRefCounted
20 {
21     /// Atomically loads the number of active references.
22     final @property size_t refCount() const {
23         return (cast(shared(AtomicRefCounted))this).refCountShared;
24     }
25     /// ditto
26     shared @property size_t refCountShared() const;
27 
28     /// Atomically increment the reference count.
29     final void retain() {
30         return (cast(shared(AtomicRefCounted))this).retainShared();
31     }
32     /// ditto
33     void retainShared() shared;
34 
35     /// Atomically decrement the reference count.
36     /// If refCount reaches zero, and disposeOnZero is set,
37     /// the object is locked with its own mutex, and dispose is called.
38     /// In most cases, the calling code should set disposeOnZero, unless it
39     /// is intended to release the object to give it away.
40     /// (such as at the end of a builder function)
41     /// Returns: true if the object was disposed during this call, false otherwise.
42     final bool release(in Flag!"disposeOnZero" disposeOnZero = Yes.disposeOnZero)
43     in {
44         assert(
45             refCount > 0,
46             "inconsistent ref count for "~(cast(Object)this).classinfo.name
47         );
48     }
49     body {
50         return (cast(shared(AtomicRefCounted))this).releaseShared(disposeOnZero);
51     }
52     /// ditto
53     bool releaseShared(in Flag!"disposeOnZero" disposeOnZero = Yes.disposeOnZero) shared
54     in {
55         assert(
56             refCountShared > 0,
57             "inconsistent ref count for "~(cast(Object)this).classinfo.name
58         );
59     }
60 
61     /// Returns whether the refCount >= 1.
62     /// This increases the refCount by 1. rcLock should be used to keep
63     /// weak reference and ensures that the resource is not disposed.
64     /// The operation is atomic.
65     final bool rcLock()
66     out(res) {
67         assert(
68             (res && refCount >= 2) || (!res && !refCount),
69             "inconsistent ref count for "~(cast(Object)this).classinfo.name
70         );
71     }
72     body {
73         return (cast(shared(AtomicRefCounted))this).rcLockShared();
74     }
75     /// ditto
76     shared bool rcLockShared();
77     // out(res) {
78     //     assert(
79     //         (res && refCountShared >= 2) || (!res && !refCountShared),
80     //         "inconsistent ref count for "~(cast(Object)this).classinfo.name
81     //     );
82     // }
83     // this contract compiles with dmd but create a failure on ldc2:
84     // cannot implicitely convert shared(T) to const(AtomicRefCounted)
85 
86     /// Dispose the underlying resource
87     void dispose()
88     in { assert(refCount == 0); }
89 }
90 
91 /// compile time check that T can be ref counted atomically.
92 enum isAtomicRefCounted(T) = is(T : shared(AtomicRefCounted)) || is(T : AtomicRefCounted);
93 
94 
95 /// A string that can be mixed-in a class declaration to implement AtomicRefCounted.
96 /// dispose is not implemented of course, but is called by release while the object is locked.
97 /// Classes implementing it are free to do it in a non-thread safe manner as long
98 /// as dispose does not manipulate external state.
99 enum atomicRcCode = sharedAtomicMethods ~ q{
100     private size_t _refCount=0;
101 };
102 
103 /// Counts the number of references of a single object.
104 size_t countObj(T)(T obj) if (isAtomicRefCounted!T)
105 {
106     static if (is(T == shared)) {
107         return obj.refCountShared;
108     }
109     else {
110         return obj.refCount;
111     }
112 }
113 
114 /// Retains a single object.
115 T retainObj(T)(T obj) if (isAtomicRefCounted!T)
116 {
117     static if (is(T == shared)) {
118         obj.retainShared();
119     }
120     else {
121         obj.retain();
122     }
123     return obj;
124 }
125 
126 /// Releases and nullify a single object. The object may be null.
127 /// Returns: whether the object was disposed.
128 bool releaseObj(T)(ref T obj, in Flag!"disposeOnZero" disposeOnZero = Yes.disposeOnZero)
129 if (isAtomicRefCounted!T)
130 {
131     if (!obj) return false;
132 
133     static if (is(T == shared)) {
134         const res = obj.releaseShared(disposeOnZero);
135         obj = null;
136         return res;
137     }
138     else {
139         const res = obj.release(disposeOnZero);
140         obj = null;
141         return res;
142     }
143 }
144 
145 /// Locks a single object.
146 T lockObj(T)(T obj) if (isAtomicRefCounted!T)
147 in {
148     assert(obj, "locking null object");
149 }
150 body {
151     static if (is(T == shared)) {
152         if (obj.rcLockShared()) {
153             return obj;
154         }
155     }
156     else {
157         if (obj.rcLock()) {
158             return obj;
159         }
160     }
161     return null;
162 }
163 
164 /// Decreases the reference count of a single object without disposing it.
165 /// Use this to move an object out of a scope (typically return at the end of a return function)
166 T giveAwayObj(T)(ref T obj) if (is(T : AtomicRefCounted))
167 in {
168     assert(obj, "giving away null object");
169 }
170 body {
171     auto o = obj;
172     releaseObj(obj, No.disposeOnZero);
173     return o;
174 }
175 
176 /// Dispose and nullify a single object, that might be null
177 void disposeObj(T)(ref T obj) if (is(T : Disposable))
178 {
179     if (obj) {
180         obj.dispose();
181         obj = null;
182     }
183 }
184 
185 /// Retain GC allocated array of ref-counted resources
186 void retainArr(T)(ref T[] arr) if (isAtomicRefCounted!T)
187 {
188     import std.algorithm : each;
189     arr.each!(el => retainObj(el));
190 }
191 
192 /// Release GC allocated array of ref-counted resources
193 void releaseArr(T)(ref T[] arr) if (isAtomicRefCounted!T)
194 {
195     import std.algorithm : each;
196     arr.each!(el => releaseObj(el));
197     arr = null;
198 }
199 
200 /// Dispose GC allocated array of resources
201 void disposeArr(T)(ref T[] arr) if (is(T : Disposable))
202 {
203     import std.algorithm : each;
204     arr.each!(el => el.dispose());
205     arr = null;
206 }
207 
208 /// Retain GC allocated associative array of ref-counted resources
209 void retainAA(T, K)(ref T[K] arr) if (isAtomicRefCounted!T)
210 {
211     import std.algorithm : each;
212     arr.each!((k, el) => retainObj(el));
213 }
214 /// Release GC allocated associative array of ref-counted resources
215 void releaseAA(T, K)(ref T[K] arr) if (isAtomicRefCounted!T)
216 {
217     import std.algorithm : each;
218     arr.each!((k, el) => releaseObj(el));
219     arr.clear();
220     arr = null;
221 }
222 
223 /// Dispose GC allocated associative array of resources
224 void disposeAA(T, K)(ref T[K] arr) if (is(T : Disposable))
225 {
226     import std.algorithm : each;
227     arr.each!((k, el) { el.dispose(); });
228     arr = null;
229 }
230 
231 /// Reinitialises a single struct
232 /// Useful if the struct release resource in its destructor.
233 void reinitStruct(T)(ref T t) if (is(T == struct))
234 {
235     t = T.init;
236 }
237 
238 /// Reinitialises a GC allocated array of struct.
239 /// Useful if the struct release resource in its destructor.
240 void reinitArr(T)(ref T[] arr) if (is(T == struct))
241 {
242     foreach(ref t; arr) {
243         t = T.init;
244     }
245     arr = null;
246 }
247 /// Reinitialises a GC allocated associative array of struct.
248 /// Useful if the struct release resource in its destructor.
249 void reinitAA(T, K)(ref T[K] arr) if (is(T == struct))
250 {
251     foreach(k, ref t; arr) {
252         t = T.init;
253     }
254     arr.clear();
255     arr = null;
256 }
257 
258 /// Helper that build a new instance of T and returns it within a Rc!T
259 template makeRc(T) if (isAtomicRefCounted!T)
260 {
261     Rc!T makeRc(Args...)(Args args)
262     {
263         return Rc!T(new T(args));
264     }
265 }
266 
267 /// Helper that places an instance of T within a Rc!T
268 template rc(T) if (isAtomicRefCounted!T)
269 {
270     Rc!T rc(T obj)
271     {
272         if (obj) {
273             return Rc!T(obj);
274         }
275         else {
276             return (Rc!T).init;
277         }
278     }
279 }
280 
281 /// Produces an Rc!T holding a null object
282 @property Rc!T nullRc(T)() if (isAtomicRefCounted!T) {
283     return Rc!T.init;
284 }
285 
286 /// Helper struct that manages the reference count of an object using RAII.
287 struct Rc(T) if (isAtomicRefCounted!T)
288 {
289     private T _obj;
290 
291     /// Build a Rc instance with the provided resource
292     this(T obj)
293     {
294         _obj = obj;
295         if (obj) {
296             retainObj(_obj);
297         }
298     }
299 
300     /// Postblit adds a reference to the held reference.
301     this(this)
302     {
303         if (_obj) retainObj(_obj);
304     }
305 
306     /// Removes a reference to the held reference.
307     ~this()
308     {
309         if(_obj) {
310             releaseObj(_obj, Yes.disposeOnZero);
311             _obj = null;
312         }
313     }
314 
315     /// Assign another resource. Release the previously held ref and retain the new one.
316     void opAssign(T obj)
317     {
318         if (obj is _obj) return;
319 
320         if(_obj) releaseObj(_obj, Yes.disposeOnZero);
321         _obj = obj;
322         if(_obj) retainObj(_obj);
323     }
324 
325     /// Check whether this Rc is assigned to a resource.
326     bool opCast(T : bool)() const
327     {
328         return loaded;
329     }
330 
331     /// Check whether this Rc is assigned to a resource.
332     @property bool loaded() const
333     {
334         return _obj !is null;
335     }
336 
337     /// Reset the resource.
338     void unload()
339     {
340         if(_obj) {
341             releaseObj(_obj);
342         }
343     }
344 
345     /// Decrease the ref count and return the object without calling dispose
346     T giveAway()
347     in {
348         assert(_obj, "giving away invalid object");
349     }
350     out(obj) {
351         assert(obj);
352     }
353     body {
354         auto obj = _obj;
355         releaseObj(_obj, No.disposeOnZero);
356         return obj;
357     }
358 
359     /// Access to the held resource.
360     @property inout(T) obj() inout { return _obj; }
361 
362     alias obj this;
363 }
364 
365 /// Helper struct that keeps a weak reference to a Resource.
366 struct Weak(T) if (is(T : AtomicRefCounted))
367 {
368     private T _obj;
369 
370     /// Build a Weak instance.
371     this(T obj)
372     {
373         _obj = obj;
374     }
375 
376     /// Reset the internal reference.
377     void reset()
378     {
379         _obj = null;
380     }
381 
382     /// Check whether the resource has been disposed.
383     @property bool disposed() const
384     {
385         return !_obj || (_obj.refCount == 0);
386     }
387 
388     /// Return a Rc that contain the underlying resource if it has not been disposed.
389     Rc!T lock()
390     {
391         Rc!T rc;
392         rc._obj = _obj ? lockObj(_obj) : null;
393         if (!rc._obj) _obj = null;
394         return rc;
395     }
396 }
397 
398 debug(rc) {
399     __gshared string rcTypeRegex = ".";
400     __gshared bool rcPrintStack = false;
401 }
402 debug {
403     shared static ~this() {
404         // ensure we trigger AtomicRefCounted dtors
405         import core.memory : GC;
406         GC.collect();
407     }
408 }
409 
410 private enum sharedAtomicMethods = q{
411 
412     import std.typecons : Flag, Yes;
413     import std.traits : Unqual;
414 
415     debug {
416         ~this() {
417             // no gc operation allowed during gc pass
418             import std.stdio : stderr;
419             enum phrase = rcTypeName ~ " was not properly disposed\n";
420             if (refCount != 0) {
421                 stderr.write(phrase);
422                 stderr.flush();
423             }
424         }
425         enum rcTypeName = Unqual!(typeof(this)).stringof;
426     }
427 
428     debug(rc) {
429         private final shared void rcDebug(Args...)(string fmt, Args args)
430         {
431             import gfx.core.rc : gfxRcLog, rcPrintStack, rcTypeRegex;
432             import gfx.core.util : StackTrace;
433             import std.algorithm : min;
434             import std.regex : matchFirst;
435 
436             if (!matchFirst(rcTypeName, rcTypeRegex)) return;
437 
438             gfxRcLog.debugf(fmt, args);
439             if (rcPrintStack) {
440                 const st = StackTrace.obtain(13, StackTrace.Options.all);
441                 const frames = st.frames.length > 2 ? st.frames[2 .. $] : [];
442                 foreach (i, f; frames) {
443                     gfxRcLog.debugf("  %02d %s", i, f.symbol);
444                 }
445             }
446         }
447     }
448 
449 
450     public final shared override @property size_t refCountShared() const
451     {
452         import core.atomic : atomicLoad;
453         return atomicLoad(_refCount);
454     }
455 
456     public final shared override void retainShared()
457     {
458         import core.atomic : atomicOp;
459         const rc = atomicOp!"+="(_refCount, 1);
460         debug(rc) {
461             rcDebug("retain %s: %s -> %s", rcTypeName, rc-1, rc);
462         }
463     }
464 
465     public final shared override bool releaseShared(in Flag!"disposeOnZero" disposeOnZero = Yes.disposeOnZero)
466     {
467         import core.atomic : atomicOp;
468         const rc = atomicOp!"-="(_refCount, 1);
469 
470         debug(rc) {
471             rcDebug("release %s: %s -> %s", rcTypeName, rc+1, rc);
472         }
473         if (rc == 0 && disposeOnZero) {
474             debug(rc) {
475                 rcDebug("dispose %s", rcTypeName);
476             }
477             synchronized(this) {
478                 // cast shared away
479                 auto obj = cast(Unqual!(typeof(this)))this;
480                 obj.dispose();
481             }
482             return true;
483         }
484         else {
485             return false;
486         }
487     }
488 
489     public final shared override bool rcLockShared()
490     {
491         import core.atomic : atomicLoad, cas;
492         while (1) {
493             const c = atomicLoad(_refCount);
494 
495             if (c == 0) {
496                 debug(rc) {
497                     rcDebug("rcLock %s: %s", rcTypeName, c);
498                 }
499                 return false;
500             }
501             if (cas(&_refCount, c, c+1)) {
502                 debug(rc) {
503                     rcDebug("rcLock %s: %s", rcTypeName, c+1);
504                 }
505                 return true;
506             }
507         }
508     }
509 };
510 
511 // private string numberizeCodeLines(string code) {
512 //     import std.string : splitLines;
513 //     import std.format : format;
514 //     string res;
515 //     auto lines = code.splitLines();
516 //     foreach (i, l; lines) {
517 //         res ~= format("%s. %s\n", i+1, l);
518 //     }
519 //     return res;
520 // }
521 
522 // pragma(msg, numberizeCodeLines(atomicRcCode));
523 
524 // using version(unittest) instead of private creates link errors
525 // in test builds in apps/libs depending on gfx-d (??)
526 private
527 {
528     int rcCount = 0;
529     int structCount = 0;
530 
531     class RcClass : AtomicRefCounted
532     {
533         mixin(atomicRcCode);
534 
535         this()
536         {
537             rcCount += 1;
538         }
539 
540         override void dispose()
541         {
542             rcCount -= 1;
543         }
544     }
545 
546     struct RcStruct
547     {
548         Rc!RcClass obj;
549     }
550 
551     struct RcArrStruct
552     {
553         Rc!RcClass[] objs;
554 
555         ~this()
556         {
557             foreach(ref o; objs) {
558                 o = Rc!RcClass.init;
559             }
560         }
561     }
562 
563     struct RcArrIndStruct
564     {
565         RcStruct[] objs;
566 
567         ~this()
568         {
569             foreach(ref o; objs) {
570                 o = RcStruct.init;
571             }
572         }
573     }
574 
575 
576     unittest
577     {
578         {
579             auto arr = RcArrStruct([makeRc!RcClass(), makeRc!RcClass()]);
580             assert(rcCount == 2);
581             foreach(obj; arr.objs) {
582                 assert(rcCount == 2);
583             }
584             assert(rcCount == 2);
585         }
586         assert(rcCount == 0);
587     }
588 
589 
590     unittest
591     {
592         {
593             auto obj = makeRc!RcClass();
594             assert(rcCount == 1);
595         }
596         assert(rcCount == 0);
597     }
598 
599     unittest
600     {
601         {
602             auto obj = RcStruct(makeRc!RcClass());
603             assert(rcCount == 1);
604         }
605         assert(rcCount == 0);
606     }
607 
608     unittest
609     {
610         Weak!RcClass weak;
611         {
612             auto locked = weak.lock();
613             assert(!locked.loaded);
614         }
615         {
616             auto rc = makeRc!RcClass();
617             assert(rcCount == 1);
618             assert(rc.refCount == 1);
619             weak = Weak!RcClass(rc);
620             assert(rc.refCount == 1);
621             {
622                 auto locked = weak.lock();
623                 assert(locked.loaded);
624                 assert(rc.refCount == 2);
625             }
626             assert(rc.refCount == 1);
627         }
628         assert(rcCount == 0);
629         assert(weak.disposed);
630         {
631             auto locked = weak.lock();
632             assert(!locked.loaded);
633         }
634     }
635 }
636 
637 static if(false)
638 {
639     /// Creates a new instance of $(D T) and returns it under a $(D Uniq!T).
640     template makeUniq(T)
641     if (is(T : Disposable))
642     {
643         Uniq!T makeUniq(Args...)(Args args)
644         {
645             return Uniq!T(new T (args));
646         }
647     }
648 
649     debug(Uniq)
650     {
651         import std.stdio;
652     }
653 
654     /// A helper struct that manage the lifetime of a Disposable using RAII.
655     /// Note: dlang has capability to enforce a parameter be a lvalue (ref param)
656     /// but has no mechanism such as c++ rvalue reference which would enforce
657     /// true uniqueness by the compiler. Uniq gives additional robustness, but it is
658     /// up to the programmer to make sure that the values passed in by rvalue are
659     /// not referenced somewhere else in the code
660     struct Uniq(T)
661     if (is(T : Disposable) && !hasMemberFunc!(T, "release"))
662     {
663         private T _obj;
664         alias Resource = T;
665 
666         // prevent using Uniq with Refcounted
667         // (invariant handles runtime polymorphism)
668         static assert(!is(T : RefCounted), "Use Rc helper for RefCounted objects");
669         invariant()
670         {
671             // if obj is assigned, it must not cast to a RefCounted
672             assert(!_obj || !(cast(RefCounted)_obj), "Use Rc helper for RefCounted objects");
673         }
674 
675         /// Constructor taking rvalue. Uniqueness is achieve only if there is no
676         /// aliases of the passed reference.
677         this(T obj)
678         {
679             debug(Uniq)
680             {
681                 writefln("build a Uniq!%s from rvalue", T.stringof);
682             }
683             _obj = obj;
684         }
685 
686         /// Constructor taking lvalue. Uniqueness is achieve only if there is no
687         /// other copies of the passed reference.
688         this(ref T obj)
689         {
690             debug(Uniq)
691             {
692                 writefln("build a Uniq!%s from lvalue", T.stringof);
693             }
694             _obj = obj;
695             obj = null;
696         }
697 
698         /// Constructor that take a rvalue.
699         /// $(D u) can only be a rvalue because postblit is disabled.
700         this(U)(Uniq!U u)
701         if (is(U : T))
702         {
703             debug(Uniq)
704             {
705                 writefln("cast building a Uniq from rvalue from %s to %s",
706                     U.stringof, T.stringof
707                 );
708             }
709             _obj = u._obj;
710         }
711 
712         /// Transfer ownership from a Uniq of a type that is convertible to our type.
713         /// $(D u) can only be a rvalue because postblit is disabled.
714         void opAssign(U)(Uniq!U u)
715         if (is(U : T))
716         {
717             debug(Uniq)
718             {
719                 writefln("opAssign a Uniq from rvalue from %s to %s",
720                     U.stringof, T.stringof
721                 );
722             }
723             if (_obj)
724             {
725                 _obj.dispose();
726             }
727             _obj = u._obj;
728             u._obj = null;
729         }
730 
731         /// Shortcut to assigned
732         bool opCast(U : bool)() const
733         {
734             return assigned;
735         }
736 
737         /// Destructor that disposes the resource.
738         ~this()
739         {
740             debug(Uniq)
741             {
742                 writefln("dtor of Uniq!%s", T.stringof);
743             }
744             dispose();
745         }
746 
747         /// A view on the underlying object.
748         @property inout(T) obj() inout
749         {
750             return _obj;
751         }
752 
753         /// Forwarding method calls and member access to the underlying object.
754         alias obj this;
755 
756         /// Transfer the ownership.
757         Uniq release()
758         {
759             debug(Uniq)
760             {
761                 writefln("release of Uniq!%s", T.stringof);
762             }
763             auto u = Uniq(_obj);
764             assert(_obj is null);
765             return u;
766         }
767 
768         /// Explicitely ispose the underlying resource.
769         void dispose()
770         {
771             // Same method than Disposeable on purpose as it disables alias this.
772             // One cannot shortcut Uniq to dispose the resource.
773             if (_obj)
774             {
775                 debug(Uniq)
776                 {
777                     writefln("dispose of Uniq!%s", T.stringof);
778                 }
779                 _obj.dispose();
780                 _obj = null;
781             }
782         }
783 
784         /// Checks whether a resource is assigned.
785         bool assigned() const
786         {
787             return _obj !is null;
788         }
789 
790         // disable copying
791         @disable this(this);
792     }
793 
794     version(unittest)
795     {
796         private int disposeCount;
797 
798         private class UniqTest : Disposable
799         {
800             override void dispose()
801             {
802                 disposeCount += 1;
803             }
804         }
805 
806         private Uniq!UniqTest produce1()
807         {
808             auto u = makeUniq!UniqTest();
809             // Returning without release is fine?
810             // It compiles and passes the test, but not recommended.
811             // return u;
812             return u.release();
813         }
814 
815         private Uniq!UniqTest produce2()
816         {
817             return makeUniq!UniqTest();
818         }
819 
820         private void consume(Uniq!UniqTest /+u+/)
821         {
822         }
823 
824         unittest
825         {
826             disposeCount = 0;
827             auto u = makeUniq!UniqTest();
828             assert(disposeCount == 0);
829             static assert (!__traits(compiles, consume(u)));
830             consume(u.release());
831             assert(disposeCount == 1);
832 
833             {
834                 auto v = makeUniq!UniqTest();
835             }
836             assert(disposeCount == 2);
837 
838             consume(produce1());
839             assert(disposeCount == 3);
840 
841             consume(produce2());
842             assert(disposeCount == 4);
843 
844             auto w = makeUniq!UniqTest();
845             w.dispose();
846             assert(disposeCount == 5);
847         }
848     }
849 
850     private template hasMemberFunc(T, string fun)
851     {
852         enum bool hasMemberFunc = is(typeof(
853         (inout int = 0)
854         {
855             T t = T.init;
856             mixin("t."~fun~"();");
857         }));
858     }
859 
860     static assert(hasMemberFunc!(RefCounted, "retain"));
861     static assert(hasMemberFunc!(Disposable, "dispose"));
862     static assert(!hasMemberFunc!(Disposable, "release"));
863 }