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 }