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 }