1 module flyweightbyid; 2 3 import std.traits : isCallable; 4 5 /// Options for Flyweight instances. 6 enum FlyweightOptions 7 { 8 /// Default options: Thread local storage, automatic reference counting. 9 none = 0, 10 /// Use global storage instead of thread local storage. 11 gshared = 1 << 0, 12 /// Don't count references. 13 noReferenceCount = 1 << 1, 14 } 15 16 /** 17 * Flyweight template. 18 * 19 * Params: 20 * T = Instance type 21 * makeFunc = Callable that receives ID as argument and returns `T` 22 * disposeFunc = Callable that receives `ref T` instance to unload it 23 * idNames = Enum or string[] with known IDs 24 * options = Flyweight options 25 */ 26 struct Flyweight(T, alias makeFunc, alias disposeFunc, alias idNames, const FlyweightOptions options = FlyweightOptions.none) 27 if (isCallable!makeFunc && isCallable!disposeFunc) 28 { 29 import std.traits : EnumMembers; 30 private enum names = getNames!(idNames); 31 private enum gshared = options & FlyweightOptions.gshared; 32 private enum shouldCountReferences = !(options & FlyweightOptions.noReferenceCount); 33 34 mixin("enum ID : uint" 35 ~ "{" 36 ~ joinNames!(names) 37 ~ "invalid" 38 ~ "}" 39 ); 40 /// Verify if ID is a valid object ID 41 static bool isValidID(ID id) 42 { 43 return id < names.length; 44 } 45 /// Verify if this is a valid Flyweight object 46 bool isValid() const 47 { 48 return isValidID(id); 49 } 50 51 /// Object ID, used for reference counting 52 private ID id = ID.invalid; 53 /// Object data 54 T object; 55 alias object this; 56 57 /// Private so that Flyweight instances with valid IDs are created by `get` and copy constructors only. 58 private this(const ID id, T object) 59 { 60 this.id = id; 61 this.object = object; 62 } 63 64 static if (gshared) 65 { 66 /// Global array of known objects. 67 __gshared private T[names.length] knownObjects; 68 static if (shouldCountReferences) 69 { 70 /// Global array of reference counts. 71 __gshared private uint[names.length] referenceCounts = 0; 72 } 73 else 74 { 75 /// Global array of booleans for marking loaded objects. 76 __gshared private bool[names.length] loadedFlags = false; 77 } 78 } 79 else 80 { 81 /// Thread local array of known objects. 82 static private T[names.length] knownObjects; 83 static if (shouldCountReferences) 84 { 85 /// Thread local array of reference counts. 86 static private uint[names.length] referenceCounts = 0; 87 } 88 else 89 { 90 /// Thread local array of booleans for marking loaded objects. 91 static private bool[names.length] loadedFlags = false; 92 } 93 } 94 95 static if (shouldCountReferences) 96 { 97 /// Copy constructor with automatic reference counting. 98 this(ref return scope inout Flyweight other) inout 99 { 100 this.id = other.id; 101 this.object = other.object; 102 if (isValid) 103 { 104 incref(id); 105 } 106 } 107 108 version (D_BetterC) {} 109 else 110 { 111 /// Postblit with automatic reference counting. 112 this(this) @nogc nothrow 113 { 114 if (isValid) 115 { 116 incref(id); 117 } 118 } 119 } 120 121 /// Destructor with automatic reference counting. 122 ~this() 123 { 124 if (isValid) 125 { 126 unref(id); 127 } 128 } 129 130 /// Manually increment reference. 131 static void incref(ID id) @nogc nothrow 132 in { assert(isValidID(id)); } 133 out { assert(referenceCounts[id] > 0); } 134 do 135 { 136 referenceCounts[id]++; 137 } 138 139 /// Manually decrement reference. 140 static void unref(ID id) 141 in { assert(isValidID(id)); } 142 do 143 { 144 if (isLoaded(id)) 145 { 146 referenceCounts[id]--; 147 if (referenceCounts[id] == 0) 148 { 149 disposeFunc(knownObjects[id]); 150 } 151 } 152 } 153 } 154 155 /// Get the Flyweight instance for object identified by `id`, constructing it if not loaded yet. 156 static Flyweight get(ID id) 157 in { assert(isValidID(id)); } 158 out { assert(isLoaded(id)); } 159 do 160 { 161 if (!isLoaded(id)) 162 { 163 knownObjects[id] = makeFunc(id); 164 static if (!shouldCountReferences) loadedFlags[id] = true; 165 } 166 static if (shouldCountReferences) incref(id); 167 return Flyweight(id, knownObjects[id]); 168 } 169 170 /// Returns if Flyweight identified by `id` is loaded of not. 171 static bool isLoaded(ID id) @nogc nothrow 172 in { assert(isValidID(id)); } 173 do 174 { 175 static if (shouldCountReferences) 176 { 177 return referenceCounts[id] > 0; 178 } 179 else 180 { 181 return loadedFlags[id]; 182 } 183 } 184 185 /// Returns whether there are any Flyweight instances loaded. 186 static bool isAnyLoaded() @nogc nothrow 187 { 188 import std.algorithm : any; 189 static if (shouldCountReferences) 190 { 191 return any!"a > 0"(referenceCounts[]); 192 } 193 else 194 { 195 return any(loadedFlags[]); 196 } 197 } 198 199 /// If Flyweight identified by `id` is loaded, manually unload it and reset reference count/loaded flag. 200 static void unload(ID id) 201 in { assert(isValidID(id)); } 202 out { assert(!isLoaded(id)); } 203 do 204 { 205 if (isLoaded(id)) 206 { 207 disposeFunc(knownObjects[id]); 208 static if (shouldCountReferences) 209 { 210 referenceCounts[id] = 0; 211 } 212 else 213 { 214 loadedFlags[id] = false; 215 } 216 } 217 } 218 219 /// Manually unload all loaded instances and reset reference counts/loaded flags. 220 static void unloadAll() 221 out { 222 foreach (id; EnumMembers!(ID)[0 .. $-1]) 223 { 224 assert(!isLoaded(id)); 225 } 226 assert(!isAnyLoaded); 227 } 228 do 229 { 230 foreach (id; EnumMembers!(ID)[0 .. $-1]) 231 { 232 unload(id); 233 } 234 } 235 236 static foreach (name; names) 237 { 238 mixin("static Flyweight " ~ normalizeName!name ~ "() { return get(ID." ~ normalizeName!name ~ "); }"); 239 } 240 } 241 242 // Private compile-time helpers 243 private template getNames(alias idNames) 244 { 245 import std.conv : to; 246 import std.range : isInputRange; 247 import std.traits : EnumMembers; 248 static if (is(idNames == enum)) 249 { 250 enum string[] getNames = [EnumMembers!(idNames)].to!(string[]); 251 } 252 else 253 { 254 private alias idNamesType = typeof(idNames); 255 static if (is(idNamesType : string)) 256 { 257 enum string[] getNames = [idNames]; 258 } 259 else static if (isInputRange!(idNamesType) && !is(idNamesType : string[])) 260 { 261 static if (__traits(compiles, { import std.array : staticArray; })) 262 { 263 import std.array : staticArray; 264 enum string[] getNames = staticArray!(idNames); 265 } 266 else 267 { 268 import std.array : array; 269 enum string[] getNames = idNames.array; 270 } 271 } 272 else 273 { 274 enum string[] getNames = idNames; 275 } 276 } 277 } 278 279 private template normalizeName(string name) 280 { 281 private string _normalizeName() 282 { 283 import std.ascii : isAlphaNum, isDigit; 284 string result; 285 foreach (c; name) 286 { 287 result ~= (isAlphaNum(c) || c == '_') ? c : '_'; 288 } 289 if (isDigit(result[0])) 290 { 291 result = '_' ~ result; 292 } 293 return result; 294 } 295 296 enum normalizeName = _normalizeName(); 297 } 298 299 private template joinNames(string[] names) 300 { 301 private string _joinNames() 302 { 303 string result; 304 static foreach (n; names) 305 { 306 result ~= normalizeName!n ~ ", "; 307 } 308 return result; 309 } 310 311 enum joinNames = _joinNames(); 312 } 313 314 version (unittest) 315 { 316 enum names = [ 317 "one", 318 "two", 319 "three", 320 ]; 321 string makeName(uint id) 322 in { assert(id < names.length); } 323 do 324 { 325 return names[id]; 326 } 327 void disposeName(ref string name) 328 { 329 import std.stdio : writeln; 330 writeln("Bye bye ", name); 331 name = null; 332 } 333 } 334 335 unittest 336 { 337 // names from string[] 338 alias NameFlyweight = Flyweight!(string, makeName, disposeName, names); 339 NameFlyweight invalid; 340 assert(!invalid.isValid); 341 342 { 343 assert(NameFlyweight.one == "one"); 344 assert(NameFlyweight.two == "two"); 345 assert(NameFlyweight.three == "three"); 346 } 347 348 { 349 const auto one1 = NameFlyweight.one; 350 const auto one2 = NameFlyweight.one; 351 const auto one3 = NameFlyweight.one; 352 assert(NameFlyweight.isLoaded(NameFlyweight.ID.one)); 353 assert(one1.object is one2.object); 354 assert(one2.object is one3.object); 355 assert(one1.object is one3.object); 356 assert(NameFlyweight.isAnyLoaded()); 357 } 358 359 assert(!NameFlyweight.isLoaded(NameFlyweight.ID.one)); 360 assert(!NameFlyweight.isLoaded(NameFlyweight.ID.two)); 361 assert(!NameFlyweight.isLoaded(NameFlyweight.ID.three)); 362 assert(!NameFlyweight.isAnyLoaded()); 363 } 364 365 unittest 366 { 367 // names from enum members 368 enum ABC { A, B, C, D, None } 369 alias ABCFlyweight = Flyweight!(string, makeName, disposeName, ABC); 370 assert(__traits(hasMember, ABCFlyweight, "A")); 371 assert(__traits(hasMember, ABCFlyweight.ID, "A")); 372 assert(__traits(hasMember, ABCFlyweight, "B")); 373 assert(__traits(hasMember, ABCFlyweight.ID, "B")); 374 assert(__traits(hasMember, ABCFlyweight, "C")); 375 assert(__traits(hasMember, ABCFlyweight.ID, "C")); 376 assert(__traits(hasMember, ABCFlyweight, "D")); 377 assert(__traits(hasMember, ABCFlyweight.ID, "D")); 378 assert(__traits(hasMember, ABCFlyweight, "None")); 379 assert(__traits(hasMember, ABCFlyweight.ID, "None")); 380 } 381 382 unittest 383 { 384 // name passed directly 385 alias SingletonFlyweight = Flyweight!(string, makeName, disposeName, "instance", FlyweightOptions.gshared); 386 assert(__traits(hasMember, SingletonFlyweight, "instance")); 387 assert(__traits(hasMember, SingletonFlyweight.ID, "instance")); 388 } 389 390 unittest 391 { 392 // names with invalid enum identifiers 393 alias MyFlyweight = Flyweight!(string, makeName, disposeName, ["First ID", "Second!", "123"]); 394 assert(__traits(hasMember, MyFlyweight, "First_ID")); 395 assert(__traits(hasMember, MyFlyweight.ID, "First_ID")); 396 assert(__traits(hasMember, MyFlyweight, "Second_")); 397 assert(__traits(hasMember, MyFlyweight.ID, "Second_")); 398 assert(__traits(hasMember, MyFlyweight, "_123")); 399 assert(__traits(hasMember, MyFlyweight.ID, "_123")); 400 } 401 402 unittest 403 { 404 // no reference counting 405 alias ABCFlyweight = Flyweight!(string, makeName, disposeName, ["A", "B", "C"], FlyweightOptions.noReferenceCount); 406 { 407 auto a = ABCFlyweight.A; 408 assert(ABCFlyweight.isLoaded(ABCFlyweight.ID.A)); 409 } 410 assert(ABCFlyweight.isLoaded(ABCFlyweight.ID.A)); 411 assert(!ABCFlyweight.isLoaded(ABCFlyweight.ID.B)); 412 auto a = ABCFlyweight.A; 413 ABCFlyweight.unload(ABCFlyweight.ID.A); 414 assert(!ABCFlyweight.isLoaded(ABCFlyweight.ID.A)); 415 ABCFlyweight.unloadAll(); 416 } 417 418 unittest 419 { 420 import std.algorithm : map; 421 import std.conv : to; 422 import std.range : iota; 423 alias NFlyweight = Flyweight!(string, makeName, disposeName, iota(3).map!(to!string)); 424 } 425 426 version (unittest) 427 { 428 // README example 429 import flyweightbyid; 430 431 // Flyweight for instances of `Image*`, loaded by `loadImage` and unloaded by `unloadImage` 432 // IDs and getter names are taken from `imageFileNames` slice 433 alias ImageFlyweight = Flyweight!( 434 Image*, 435 loadImage, 436 unloadImage, 437 imageFileNames, 438 /+, FlyweightOptions.none /+ (the default) +/ +/ 439 ); 440 441 // Some file names that should be loaded only once 442 enum imageFileNames = [ 443 "img1.png", 444 "subdir/img2.png", 445 ]; 446 // Image struct, with a pointer to the data, dimensions, member functions, etc... 447 struct Image { 448 void draw() const 449 { 450 // ... 451 } 452 // ... 453 ~this() 454 { 455 import std.stdio : writeln; 456 writeln("bye bye"); 457 } 458 } 459 460 // Function that loads an Image from file 461 Image* loadImage(uint id) 462 { 463 auto filename = imageFileNames[id]; 464 Image* img = new Image; 465 // ... 466 return img; 467 } 468 // Function to unload the images 469 void unloadImage(ref Image* img) 470 { 471 // ... 472 destroy(img); 473 } 474 } 475 unittest 476 { 477 // Flyweight identified by `ID.img1_png` is constructed by calling `loadImage(0)` 478 // Notice how invalid identifier characters are replaced by underscores 479 ImageFlyweight image1 = ImageFlyweight.get(ImageFlyweight.ID.img1_png); 480 assert(ImageFlyweight.isLoaded(ImageFlyweight.ID.img1_png)); 481 482 // `img1_png` is an alias for getting the "img1.png" instance, 483 // `subdir_img2_png` for "subdir/img2.png" and so on 484 auto also_image1 = ImageFlyweight.img1_png; 485 486 // `also_image1` contains the same instance as `image1`, as it is already loaded 487 assert(also_image1 is image1); 488 489 { 490 // `ID.subdir_img2_png` is constructed by `loadImage(1)` 491 ImageFlyweight image2 = ImageFlyweight.subdir_img2_png; 492 493 // ImageFlyweight instance is a proxy (by means of `alias this`) 494 // for the loaded `Image*` instance, so member functions, fields and 495 // others work like expected 496 image2.draw(); 497 498 // If `FlyweightOptions.noReferenceCount` is NOT passed to template (default), 499 // references are automatically counted and content is unloaded if reference 500 // count reaches 0. Pass them by value for automatic reference counting 501 ImageFlyweight also_image2 = image2; 502 503 assert(ImageFlyweight.isLoaded(ImageFlyweight.ID.subdir_img2_png)); 504 // subdir_img2_png gets unloaded 505 } 506 assert(!ImageFlyweight.isLoaded(ImageFlyweight.ID.subdir_img2_png)); 507 508 // It is possible to manually unload one or all instances, be careful to not access them afterwards! 509 ImageFlyweight.unload(ImageFlyweight.ID.img1_png); 510 ImageFlyweight.unloadAll(); 511 // It is safe to call unload more than once, so when `image1` and `also_image1` 512 // are destroyed, nothing happens 513 assert(!ImageFlyweight.isLoaded(ImageFlyweight.ID.img1_png)); 514 assert(!ImageFlyweight.isLoaded(ImageFlyweight.ID.subdir_img2_png)); 515 } 516 517 version (unittest) 518 { 519 // README example 520 import flyweightbyid; 521 522 // Config singleton, using global storage and not reference counted 523 alias ConfigSingleton = Flyweight!( 524 Config*, 525 loadConfig, 526 unloadConfig, 527 "instance", 528 FlyweightOptions.gshared | FlyweightOptions.noReferenceCount 529 ); 530 531 // Configuration structure 532 struct Config 533 { 534 // ... 535 } 536 Config* loadConfig(uint) 537 { 538 return new Config; 539 } 540 void unloadConfig(ref Config* c) 541 { 542 destroy(c); 543 } 544 } 545 unittest 546 { 547 assert(!ConfigSingleton.isLoaded(ConfigSingleton.ID.instance)); 548 { 549 // Get Config instance 550 auto config = ConfigSingleton.instance; 551 552 auto also_config = ConfigSingleton.get(ConfigSingleton.ID.instance); 553 assert(also_config is config); 554 555 assert(ConfigSingleton.isLoaded(ConfigSingleton.ID.instance)); 556 } 557 // ConfigSingleton is not reference counted, so it is still loaded 558 assert(ConfigSingleton.isLoaded(ConfigSingleton.ID.instance)); 559 ConfigSingleton.unloadAll(); 560 assert(!ConfigSingleton.isLoaded(ConfigSingleton.ID.instance)); 561 }