1 /**
2 Dplug's wren bridge. 
3 
4 Copyright: Guillaume Piolat 2021.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module dplug.wren.wrensupport;
8 
9 import core.stdc.string : memcpy, strlen, strcmp;
10 import core.stdc.stdlib : malloc, free;
11 import core.stdc.stdio : snprintf;
12 
13 import std.traits: getSymbolsByUDA, isFunction;
14 import std.meta: staticIndexOf;
15 
16 import dplug.core.nogc;
17 import dplug.core.file;
18 import dplug.gui.context;
19 import dplug.gui.element;
20 import dplug.graphics.color;
21 
22 import wren.vm;
23 import wren.value;
24 import wren.primitive;
25 import wren.common;
26 import dplug.wren.describe;
27 import dplug.wren.wren_ui;
28 
29 nothrow @nogc:
30 
31 ///
32 /// `WrenSupport` manages interaction between Wren and the plugin. 
33 /// Such an object is created/destroyed/accessed with `enableWrenSupport()`, `disableWrenSupport()`,
34 /// and `wrenSupport()`. It is held, as all GUI globals in the `UIContext`.
35 ///
36 /// Example:
37 /// ---
38 /// // Inside your UI constructor
39 /// mixin(fieldIdentifiersAreIDs!DistortGUI);
40 /// context.enableWrenSupport();
41 /// debug
42 ///     context.wrenSupport.addModuleFileWatch("plugin", `/absolute/path/to/my/plugin.wren`); // Live-reload
43 /// else
44 ///     context.wrenSupport.addModuleSource("plugin", import("plugin.wren"));                 // Final release has static scripts
45 /// context.wrenSupport.registerScriptExports!DistortGUI;
46 /// context.wrenSupport.callCreateUI();
47 ///
48 /// // Inside your UI destructor
49 /// context.disableWrenSupport();
50 /// ---
51 ///
52 /// See_also: `enableWrenSupport()`, `disableWrenSupport()`
53 ///
54 final class WrenSupport
55 {
56 @nogc:
57 
58     /// Constructor. Use `context.enableWrenSupport()` instead.
59     this(IUIContext uiContext) nothrow
60     {
61         _uiContext = uiContext; // Note: wren VM start is deferred to first use.
62     }
63 
64     /// Instantiate that with your main UI widget to register widgets classes.
65     /// Foreach member variable of `GUIClass` with the `@ScriptExport` attribute, this registers
66     /// a Wren class 
67     /// The right Wren class can then be returned by the `$` operator and `UI.getElementById` methods.
68     ///
69     /// Note: the mirror Wren classes don't inherit from each other. Wren doesn't know our D hierarchy.
70     void registerScriptExports(GUIClass)() nothrow
71     {
72         // Automatically set widgets ID. _member.id = "_member";
73         static foreach(m; getSymbolsByUDA!(GUIClass, ScriptExport))
74         {{
75             alias dClass = typeof(m);
76             registerUIElementClass!dClass();
77         }}
78     }
79 
80     /// Add a UIElement derivative class into the set of known classes in Wren, and all its parent up to UIElement
81     /// It is recommended that you use `@ScriptExport` on your fields and `registerScriptExports` instead, but this
82     /// can be used to register classes manually.
83     ///
84     /// Note: the mirror Wren classes don't inherit from each other. Wren doesn't know our D hierarchy.
85     void registerUIElementClass(ElemClass)() nothrow
86     {
87         // If you fail here: ony UIElement derivatives can have @ScriptExport
88         // Maybe you can expose this functionnality in a widget?
89         static assert(is(ElemClass: UIElement));
90 
91         string name = ElemClass.classinfo.name;
92 
93         bool alreadyKnown = false;
94 
95         // search if class already known
96         IdentifierBloom.HashResult hash = IdentifierBloom.hashOf(name);
97         if (_identifierBloom.couldContain(hash))
98         {
99             foreach(e; _exportedClasses[])
100             {
101                 if (e.fullClassName() == name)
102                 {
103                     alreadyKnown = true;
104                     break;
105                 }
106             }
107         }
108 
109         if (!alreadyKnown)
110         {
111             _identifierBloom.add(hash);
112             ScriptExportClass c = mallocNew!ScriptExportClass(ElemClass.classinfo);
113             registerDClass!ElemClass(c);
114             _exportedClasses ~= c;
115         }
116     }
117   
118     /// Add a read-only Wren module source code, to be loaded once and never changed.
119     /// Release purpose. When in developement, you might prefer a reloadable file, with `addModuleFileWatch`.
120     void addModuleSource(const(char)[] moduleName, const(char)[] moduleSource) nothrow
121     {
122         // This forces a zero terminator.
123         char* moduleNameZ = stringDup(CString(moduleName).storage).ptr;
124         char* moduleSourceZ = stringDup(CString(moduleSource).storage).ptr;
125 
126         PreloadedSource ps;
127         ps.moduleName = moduleNameZ;
128         ps.source = moduleSourceZ;
129         _preloadedSources.pushBack(ps);
130     }
131 
132     /// Add a dynamic reloadable .wren source file. The module is reloaded and compared when `reloadScriptsThatChanged()` is called.
133     /// Development purpose. For a release plug-in, you want to use `addModuleSource` instead.
134     void addModuleFileWatch(const(char)[] moduleName, const(char)[] wrenFilePath) nothrow
135     {
136         // This forces a zero terminator.
137         char* moduleNameZ = stringDup(CString(moduleName).storage).ptr;
138         char* wrenFilePathZ = stringDup(CString(wrenFilePath).storage).ptr;
139 
140         FileWatch fw;
141         fw.moduleName = moduleNameZ;
142         fw.wrenFilePath = wrenFilePathZ;
143         fw.lastSource = null;
144 
145         _fileSources.pushBack(fw);
146     }
147 
148     /// To call in your UI's constructor. This call the `Plugin.createUI` Wren method, in module "plugin".
149     ///
150     /// Note: Changing @ScriptProperty values does not make the elements dirty.
151     ///       But since it's called at UI creation, the whole UI is dirty anyway so it works.
152     ///
153     /// Returns: `true` if no runtime or compile-time error.
154     bool callCreateUI() nothrow
155     {
156         return callPluginMethod("createUI");
157     }
158 
159     /// To call in your UI's `reflow()` method. This call the `Plugin.reflow` Wren method, in module "plugin".
160     ///
161     /// Note: Changing @ScriptProperty values does not make the elements dirty.
162     ///       But since it's called at UI reflow, the whole UI is dirty anyway so it works.
163     ///
164     /// Returns: `true` if no runtime or compile-time error.
165     bool callReflow() nothrow
166     {
167         return callPluginMethod("reflow");
168     }
169 
170     /// Read live-reload .wren files and restart the Wren VM if they have changed.
171     /// Check the registered .wren file and check if they have changed.
172     /// Since it (re)starts the Wren VM, it cannot be called from Wren.
173     /// Returns: `true` on first load, or if the scripts have changed.
174     bool reloadScriptsThatChanged() nothrow
175     {
176         bool oneScriptChanged = false;
177         foreach(ref fw; _fileSources)
178         {
179             if (fw.updateAndReturnIfChanged())
180                 oneScriptChanged = true;
181         }
182 
183         // If a script changed, we need to restart the whole Wren VM since there is no way to forger about a module!
184         if (oneScriptChanged)
185             stopWrenVM();
186 
187         // then ensure Wren VM is on
188         startWrenVM();
189         return oneScriptChanged;
190     }
191 
192     /// Call this in your `onAnimate` callback. This polls script regularly and force a full redraw.
193     ///
194     /// Example:
195     /// ---
196     /// override void onAnimate(double dt, double time)
197     /// {
198     ///    context.wrenSupport.callReflowWhenScriptsChange(dt);
199     /// }
200     /// ---
201     ///
202     /// Returns: `true` if no runtime or compile-time error.
203     bool callReflowWhenScriptsChange(double dt) nothrow
204     {
205         enum CHECK_EVERY_N_SECS = 0.2; // 200 ms
206         _timeSinceLastScriptCheck += dt;
207         if (_timeSinceLastScriptCheck > CHECK_EVERY_N_SECS)
208         {
209             _timeSinceLastScriptCheck = 0;
210             if (reloadScriptsThatChanged())
211             {
212                 // We detected a change, we need to call Plugin.reflow() in Wren.
213                 return callReflow();
214             }
215         }
216         return true;
217     }
218 
219     // <advanced API>
220 
221     /// Call Plugin.<methodName>, a static method without arguments in Wren. For advanced users only.
222     /// Returns: `true` if no runtime or compile-time error.
223     bool callPluginMethod(const(char)* methodName) nothrow
224     {
225         reloadScriptsThatChanged();
226         enum int MAX = 64+MAX_VARIABLE_NAME*2;
227         char[MAX] code;
228         snprintf(code.ptr, MAX, "{\n \nimport \"plugin\" for Plugin\n Plugin.%s()\n}\n", methodName);
229         return interpret(code.ptr);
230     }
231 
232     /// Interpret arbitrary code. For advanced users only.
233     /// Returns: `true` if no runtime or compile-time error.
234     bool interpret(const(char)* path, const(char)* source) nothrow
235     {
236         WrenInterpretResult result = wrenInterpret(_vm, path, source);
237         return result == WrenInterpretResult.WREN_RESULT_SUCCESS;
238     }
239 
240     /// Interpret arbitrary code. For advanced users only.
241     /// Returns: `true` if no runtime or compile-time error.
242     bool interpret(const(char)* source) nothrow
243     {
244         return interpret("", source);
245     }  
246 
247     // </advanced API>
248 
249     ~this() nothrow
250     {
251         stopWrenVM();
252 
253         foreach(ps; _preloadedSources[])
254         {
255             free(ps.moduleName);
256             free(ps.source);
257         }
258 
259         foreach(fw; _fileSources[])
260         {
261             free(fw.moduleName);
262             free(fw.wrenFilePath);
263             free(fw.lastSource);
264         }
265 
266         foreach(ec; _exportedClasses[])
267         {
268             destroyFree(ec);
269         }
270     }
271 
272 package:
273 
274     IUIContext uiContext() nothrow
275     {
276         return _uiContext;
277     }
278 
279     ScriptPropertyDesc* getScriptProperty(int nthClass, int nthProp) nothrow
280     {
281         ScriptExportClass sec = _exportedClasses[nthClass];
282         ScriptPropertyDesc[] descs = sec.properties();
283         return &descs[nthProp];
284     }
285 
286 private:
287 
288     WrenVM* _vm = null;
289     IUIContext _uiContext;
290     double _timeSinceLastScriptCheck = 0; // in seconds
291     IdentifierBloom _identifierBloom;
292 
293     static struct PreloadedSource
294     {
295         char* moduleName;
296         char* source;
297     }
298 
299     static struct FileWatch
300     {
301     nothrow:
302     @nogc:
303         char* moduleName;
304         char* wrenFilePath;
305         char* lastSource;
306 
307         bool updateAndReturnIfChanged()
308         {
309             // FUTURE: eventually use stat to get date of change instead
310             char* newSource = readWrenFile(); 
311             
312             // Sometimes reading the file fails, and then we should just retry later. Report no changes.
313             if (newSource is null)
314                 return false;
315 
316             if ((lastSource is null) || strcmp(lastSource, newSource) != 0)
317             {
318                 free(lastSource);
319                 lastSource = newSource;
320                 return true;
321             }
322             else
323             {
324                 free(newSource);
325                 return false;
326             }
327         }
328 
329         char* readWrenFile()
330         {
331             ubyte[] content = readFile(wrenFilePath);
332             return cast(char*) content.ptr; // correct because readFile return one trailing '\0'
333         }
334     }
335 
336     /// All known premade modules.
337     Vec!PreloadedSource _preloadedSources;
338 
339     /// All known file-watch modules.
340     Vec!FileWatch _fileSources;
341 
342     /// All known D @ScriptExport classes.
343     Vec!ScriptExportClass _exportedClasses;
344 
345     /// "widgets" module source, recreated on import based upon _exportedClasses content.
346     Vec!char _widgetModuleSource;
347 
348     /// The number of time a Wren VM has been started. This is to invalidate caching of Wren values.
349     uint _vmGeneration = 0;
350 
351     /// Wren module, its look-up is cached to speed-up $ operator.
352     ObjModule* _cachedUIModule,
353                _cachedWidgetsModule;
354 
355     // ui.Element class, its look-up is cached to speed-up $ operator.
356     ObjClass* _cachedClassElement;
357 
358     void startWrenVM() nothrow
359     {
360         if (_vm !is null)
361             return; // Already started
362 
363         WrenConfiguration config;
364         wrenInitConfiguration(&config);
365 
366         config.writeFn             = &dplug_wrenPrint;
367         config.errorFn             = &dplug_wrenError;
368         config.dollarOperatorFn    = &dplug_wrenDollarOperator;
369         config.bindForeignMethodFn = &dplug_wrenBindForeignMethod;
370         config.bindForeignClassFn  = &dplug_wrenBindForeignClass;
371         config.loadModuleFn        = &dplug_wrenLoadModule;
372         config.userData            = cast(void*)this;
373 
374         // Makes a bit easier to copy/paste code from D to Wren and vice-versa.
375         // Of course this isn't vanilla Wren, but too much time were spent chasing and 
376         // eliminating semicolons in Wren code.
377         config.acceptsTrailingSemicolons = true;
378 
379         // Note: wren defaults for memory usage make a lot of sense.
380         _vm = wrenNewVM(&config);
381         _vmGeneration++;
382         _cachedUIModule = null;
383         _cachedWidgetsModule = null;
384         _cachedClassElement = null;
385     }
386 
387     void stopWrenVM() nothrow
388     {
389         if (_vm !is null)
390         {
391             wrenFreeVM(_vm);
392             _vm = null;
393         }
394     }
395 
396     void print(const(char)* text) nothrow
397     {
398         debugLog(text);
399     }
400 
401     void error(WrenErrorType type, const(char)* module_, int line, const(char)* message) nothrow
402     {
403         switch(type)
404         {
405             case WrenErrorType.WREN_ERROR_COMPILE:
406                 debugLogf("%s(%d): Error: %s\n", module_, line, message);
407                 break;
408 
409             case WrenErrorType.WREN_ERROR_RUNTIME:
410                 debugLogf("wren crash: %s\n", message);
411                 break;
412 
413             case WrenErrorType.WREN_ERROR_STACK_TRACE:
414                 debugLogf("  %s.%s:%d\n", module_, message, line);
415                 break;
416 
417             default:
418                 assert(false);
419         }
420     }
421 
422     // this is called anytime Wren looks for a foreign method
423     WrenForeignMethodFn foreignMethod(WrenVM* vm, const(char)* module_, const(char)* className, bool isStatic, const(char)* signature) nothrow
424     {
425         // Introducing the Dplug-Wren standard library here.
426         if (strcmp(module_, "ui") == 0)
427         {
428             return wrenUIBindForeignMethod(vm, className, isStatic, signature);
429         }
430         return null;
431     }
432 
433     // this is called anytime Wren looks for a foreign class
434     WrenForeignClassMethods foreignClass(WrenVM* vm, const(char)* module_, const(char)* className) nothrow
435     {
436         if (strcmp(module_, "ui") == 0)
437             return wrenUIForeignClass(vm, className);
438 
439         WrenForeignClassMethods methods;
440         methods.allocate = null;
441         methods.finalize = null;
442         return methods;
443     }
444 
445     WrenLoadModuleResult loadModule(WrenVM* vm, const(char)* name) nothrow
446     {
447         WrenLoadModuleResult res;
448         res.source = null;
449         res.onComplete = null;
450         res.userData = null;
451 
452         // Try file-watch source first
453         foreach(fw; _fileSources[])
454         {
455             if (strcmp(name, fw.moduleName) == 0)
456             {
457                 // Should have parsed the file preventively.
458                 // If you assert here, it probably means that your Wren file was
459                 // passed with an incorrect absolute path.
460                 assert(fw.lastSource); 
461 
462                 res.source = fw.lastSource;
463                 goto found;
464             }
465         }
466 
467         // Try preloaded source first
468         foreach(ps; _preloadedSources[])
469         {
470             if (strcmp(name, ps.moduleName) == 0)
471             {
472                 res.source = ps.source;
473                 goto found;
474             }
475         }
476 
477 
478         if (strcmp(name, "widgets") == 0)
479             res.source = widgetModuleSource();
480 
481         if (strcmp(name, "ui") == 0)
482             res.source = wrenUIModuleSource();
483         found:
484         return res;
485     }
486 
487     // Note: return a value whose lifetime is tied to Wren VM.
488     ObjModule* getWrenModule(const(char)* name) nothrow
489     {
490         Value moduleName = wrenStringFormat(_vm, "$", name);
491         wrenPushRoot(_vm, AS_OBJ(moduleName));
492         ObjModule* wModule = getModule(_vm, moduleName);
493         wrenPopRoot(_vm);
494         return wModule;
495     }
496 
497     ObjClass* getWrenClassForThisUIELement(UIElement elem) nothrow
498     {
499         // Do we have a valid cached ObjClass* inside the UIElement?
500         void* cachedClass = elem.getUserPointer(UIELEMENT_POINTERID_WREN_EXPORTED_CLASS);
501         if (cachedClass !is null)
502         {
503             // Same Wren VM?
504             uint cacheGen = cast(uint) elem.getUserPointer(UIELEMENT_POINTERID_WREN_VM_GENERATION);
505             if (cacheGen == _vmGeneration)
506             {
507                 // yes, reuse
508                 return cast(ObjClass*) cachedClass; 
509             }
510         }
511 
512         enum UIELEMENT_POINTERID_WREN_EXPORTED_CLASS = 0; /// The cached Wren class of this UIElement.
513         enum UIELEMENT_POINTERID_WREN_VM_GENERATION  = 1; /// The Wren VM count, as it is restarted. Stored as void*, but is an uint.
514 
515         // try to find concrete class directly
516         ObjClass* classTarget;
517         ScriptExportClass concreteClassInfo = findExportedClassByClassInfo(elem.classinfo);
518         if (concreteClassInfo)
519         {
520             const(char)* wrenClassName = concreteClassInfo.wrenClassNameZ();
521             classTarget = AS_CLASS(wrenFindVariable(_vm, _cachedWidgetsModule, wrenClassName));
522         }
523         else
524         {
525             // $ return a UIElement, no classes were found
526             // This is a silent error, properties assignment will silently fail.
527             // We still cache that incorrect value, as the real fix is registering the class to Wren.
528             classTarget = AS_CLASS(wrenFindVariable(_vm, _cachedUIModule, "UIElement"));
529         }
530 
531         // Cached return value inside the UIElement, which will speed-up future $
532         elem.setUserPointer(UIELEMENT_POINTERID_WREN_VM_GENERATION, classTarget);
533         elem.setUserPointer(UIELEMENT_POINTERID_WREN_VM_GENERATION, cast(void*) _vmGeneration);
534         return classTarget;
535     }
536 
537     // Implementation of the $ operator.
538     bool dollarOperator(WrenVM* vm, Value* args) nothrow
539     {
540         if (!IS_STRING(args[0]))
541             return RETURN_NULL(args);
542 
543         const(char)* id = AS_STRING(args[0]).value.ptr;
544 
545         // Note: elem can be null here. If no element is found with a proper ID,
546         // return an unbound value.
547         UIElement elem = _uiContext.getElementById(id);
548         if (elem is null)
549             return RETURN_NULL(args); // $(id) not found
550 
551         // Find a Wren class we have to convert it to.
552         // $ can be any of the imported classes in "widgets", if not it is an UIElement.
553         // Note that both "ui" and "widgets" module MUST be imported.
554 
555         if (_cachedUIModule is null)
556         {
557             _cachedUIModule = getWrenModule("ui");
558             if (_cachedUIModule is null)
559                 return RETURN_ERROR(vm, "module \"ui\" is not imported");
560         }
561 
562         if (_cachedWidgetsModule is null)
563         {
564             _cachedWidgetsModule = getWrenModule("widgets");
565             if (_cachedWidgetsModule is null)
566                 return RETURN_ERROR(vm, "module \"widgets\" is not imported");
567         }
568 
569         ObjClass* classTarget = getWrenClassForThisUIELement(elem);
570 
571         if (_cachedClassElement is null)
572         {
573             _cachedClassElement = AS_CLASS(wrenFindVariable(vm, _cachedUIModule, "Element"));
574         }
575 
576         if (classTarget is null)
577         {
578             return RETURN_ERROR(vm, "cannot create a IUElement from operator $");
579         }
580 
581         Value obj = wrenNewInstance(vm, classTarget);
582 
583         // PERF: could this be also cached? It wouldn't be managed by Wren.
584         // Create new Element foreign
585         ObjForeign* foreign = wrenNewForeign(vm, _cachedClassElement, UIElementBridge.sizeof);
586         UIElementBridge* bridge = cast(UIElementBridge*) foreign.data.ptr;
587         bridge.elem = elem; 
588 
589         // Assign it in the first field of the newly created ui.UIElement
590         ObjInstance* instance = AS_INSTANCE(obj);
591         instance.fields[0] = OBJ_VAL(foreign);
592 
593         return RETURN_OBJ(args, AS_INSTANCE(obj));
594     }
595 
596     // auto-generate the "widgets" Wren module
597     const(char)* widgetModuleSource() nothrow
598     {
599         _widgetModuleSource.clearContents();
600 
601         void text(const(char)[] s)
602         {
603             _widgetModuleSource.pushBack(cast(char[])s); // const_cast here
604         }
605 
606         void textZ(const(char)* s)
607         {
608             _widgetModuleSource.pushBack(cast(char[])s[0..strlen(s)]); // const_cast here
609         }
610 
611         void LF()
612         {
613             _widgetModuleSource.pushBack('\n');
614         }
615 
616         text(`import "ui" for UIElement, RGBA`); LF;
617 
618         foreach(size_t nthClass, ec; _exportedClasses[])
619         {
620             text("class "); textZ(ec.wrenClassNameZ); text(" is UIElement {"); LF;
621 
622             char[16] bufC;
623             snprintf(bufC.ptr, 16, "%d", cast(int)nthClass);
624 
625             foreach(size_t nth, prop; ec.properties())
626             {
627                 char[16] buf;
628                 snprintf(buf.ptr, 16, "%d", cast(int)nth);
629 
630                 bool isRGBA = prop.type == ScriptPropertyType.RGBA;
631 
632                 if (isRGBA)
633                 {
634                     // getter
635                     // PERF: this calls the wren function 4 times
636                     text("  "); text(prop.identifier); text("{"); LF;
637                     text("    return RGBA.new( e.getPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",0),");
638                                          text("e.getPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",1),");
639                                          text("e.getPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",2),");
640                                          text("e.getPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",3))"); LF;
641                     text("  }"); LF;
642 
643                     // setter for a RGBA property
644                     text("  "); text(prop.identifier); text("=(c){"); LF;
645                     text("    e.setPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",c.r, c.g, c.b, c.a)"); LF;
646                     text("  }"); LF;
647 
648                     // same but return this for chaining syntax
649                     text("  "); text(prop.identifier); text("(c){"); LF;
650                     text("    e.setPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",c.r, c.g, c.b, c.a)"); LF;
651                     text("    return this"); LF;
652                     text("  }"); LF;
653                 }
654                 else
655                 {
656                     // getter
657                     text("  "); text(prop.identifier); text("{"); LF;
658                     text("    return e.getProp_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(")"); LF;
659                     text("  }"); LF;
660 
661                     // setter for property (itself a Wren property setter)
662                     text("  "); text(prop.identifier); text("=(x){"); LF;
663                     text("    e.setProp_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",x)"); LF;
664                     text("  }"); LF;
665 
666                      // same but return this for chaining syntax
667                     text("  "); text(prop.identifier); text("(x){"); LF;
668                     text("    e.setProp_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",x)"); LF;
669                     text("    return this"); LF;
670                     text("  }"); LF;
671                 }
672             }
673             LF;
674             text("}"); LF; LF;
675         }
676 
677         _widgetModuleSource.pushBack('\0');
678         return _widgetModuleSource.ptr;
679     }
680 
681     ScriptExportClass findExportedClassByClassInfo(TypeInfo_Class info) nothrow
682     {
683         // PERF: again, not a simple lookup bt O(num-classes)
684         foreach(ScriptExportClass sec; _exportedClasses[])
685         {
686             if (sec.concreteClassInfo is info)
687             {
688                 // Found
689                 return sec;
690             }
691         }
692         return null;
693     }
694 
695     // Add a single UIElement derivative class into the set of known classes in Wren
696     // Note that it enumerates all @ScriptProperty from its ancestors too, so it works.
697     // Wren code doesn't actually know that UIImageKnob is derived from UIKnob.
698     private void registerDClass(alias aClass)(ScriptExportClass classDesc) nothrow
699     {
700         foreach(memberName; __traits(allMembers, aClass))
701         {
702             alias P = __traits(getMember, aClass, memberName);
703 
704             static if (!isFunction!P)
705             {{
706                 foreach(attr; __traits(getAttributes, P))
707                 {{
708                     static if(is(attr == ScriptProperty))
709                     {
710                         alias FieldType = typeof(P);
711 
712                         enum string fieldName = P.stringof;
713                         enum size_t offsetInClass = P.offsetof;
714 
715                         ScriptPropertyDesc desc;
716                         desc.identifier = fieldName;
717                         desc.offset = offsetInClass;
718                         static if (is(FieldType == enum))
719                         {
720                             // Note: enum are just integers in Wren, no translation of enum value.
721                             static if (FieldType.sizeof == 1)
722                                 desc.type = ScriptPropertyType.byte_;
723                             else static if (FieldType.sizeof == 2)
724                                 desc.type = ScriptPropertyType.short_;
725                             else static if (FieldType.sizeof == 4)
726                                 desc.type = ScriptPropertyType.int_;
727                             else
728                                 static assert(false, "Unsupported enum size in @ScriptProperty field " ~ fieldName ~  " of type " ~ FieldType.stringof);
729                         }
730                         else static if (is(FieldType == bool))
731                             desc.type = ScriptPropertyType.bool_;
732                         else static if (is(FieldType == RGBA))
733                             desc.type = ScriptPropertyType.RGBA;
734                         else static if (is(FieldType == ubyte))
735                             desc.type = ScriptPropertyType.ubyte_;
736                         else static if (is(FieldType == byte))
737                             desc.type = ScriptPropertyType.byte_;
738                         else static if (is(FieldType == ushort))
739                             desc.type = ScriptPropertyType.ushort_;
740                         else static if (is(FieldType == short))
741                             desc.type = ScriptPropertyType.short_;
742                         else static if (is(FieldType == uint))
743                             desc.type = ScriptPropertyType.uint_;
744                         else static if (is(FieldType == int))
745                             desc.type = ScriptPropertyType.int_;
746                         else static if (is(FieldType == float))
747                             desc.type = ScriptPropertyType.float_;
748                         else static if (is(FieldType == double))
749                             desc.type = ScriptPropertyType.double_;
750                         else static if (is(FieldType == L16)) // Note: this is temporary. TODO L16 properties should be eventually replaced by ushort instead.
751                             desc.type = ScriptPropertyType.ushort_;
752                         else
753                             static assert(false, "No @ScriptProperty support for field " ~ fieldName ~  " of type " ~ FieldType.stringof); // FUTURE: a way to add other types for properties?
754 
755                         classDesc.addProperty(desc);
756                     }
757                 }}
758             }}
759         }
760     }
761 }
762 
763 private:
764 
765 
766 void dplug_wrenPrint(WrenVM* vm, const(char)* text)
767 {
768     WrenSupport ws = cast(WrenSupport) vm.config.userData;
769     ws.print(text);
770 }
771 
772 void dplug_wrenError(WrenVM* vm, WrenErrorType type, const(char)* module_, int line, const(char)* message)
773 {
774     WrenSupport ws = cast(WrenSupport) vm.config.userData;
775     ws.error(type, module_, line, message);
776 }
777 
778 WrenForeignMethodFn dplug_wrenBindForeignMethod(WrenVM* vm, const(char)* module_, const(char)* className, bool isStatic, const(char)* signature)
779 {
780     WrenSupport ws = cast(WrenSupport) vm.config.userData;
781     return ws.foreignMethod(vm, module_, className, isStatic, signature);
782 }
783 
784 WrenForeignClassMethods dplug_wrenBindForeignClass(WrenVM* vm, const(char)* module_, const(char)* className)
785 {
786     WrenSupport ws = cast(WrenSupport) vm.config.userData;
787     return ws.foreignClass(vm, module_, className);
788 }
789 
790 WrenLoadModuleResult dplug_wrenLoadModule(WrenVM* vm, const(char)* name)
791 {
792     WrenSupport ws = cast(WrenSupport) vm.config.userData;
793     return ws.loadModule(vm, name);
794 }
795 
796 bool dplug_wrenDollarOperator(WrenVM* vm, Value* args)
797 {
798     WrenSupport ws = cast(WrenSupport) vm.config.userData;
799     return ws.dollarOperator(vm, args);
800 }
801 
802 
803 // Bloom filter to filter out if an identifier is not defined, quickly.
804 // Using the 64-bit FNV-1 hash: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
805 struct IdentifierBloom
806 {
807 pure nothrow @nogc @safe:
808     alias HashResult = ulong;
809 
810     ulong bits = 0;
811 
812     static HashResult hashOf(const(char)[] identifier)
813     {
814         ulong hash = 0xcbf29ce484222325;
815 
816         foreach(char ch; identifier)
817         {
818             hash = hash * 0x00000100000001B3;
819             hash = hash ^ cast(ulong)(ch);
820         }
821 
822         return hash;
823     }
824 
825     bool couldContain(HashResult identifierHash)
826     {
827         return (_bits & identifierHash) == identifierHash;
828     }
829 
830     void add(HashResult identifierHash)
831     {
832         _bits |= identifierHash;
833     }
834 
835 private:
836     ulong _bits = 0;
837 }