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;
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         try
237         {
238             WrenInterpretResult result = wrenInterpret(_vm, path, source);
239             return result == WrenInterpretResult.WREN_RESULT_SUCCESS;
240         }
241         catch(Exception e)
242         {
243             // Note: error reported by another mechanism anyway (debug output).
244             destroyFree(e);
245             return false;
246         }
247     }
248 
249     /// Interpret arbitrary code. For advanced users only.
250     /// Returns: `true` if no runtime or compile-time error.
251     bool interpret(const(char)* source) nothrow
252     {
253         return interpret("", source);
254     }  
255 
256     // </advanced API>
257 
258     ~this() nothrow
259     {
260         stopWrenVM();
261 
262         foreach(ps; _preloadedSources[])
263         {
264             free(ps.moduleName);
265             free(ps.source);
266         }
267 
268         foreach(fw; _fileSources[])
269         {
270             free(fw.moduleName);
271             free(fw.wrenFilePath);
272             free(fw.lastSource);
273         }
274 
275         foreach(ec; _exportedClasses[])
276         {
277             destroyFree(ec);
278         }
279     }
280 
281 package:
282 
283     IUIContext uiContext() nothrow
284     {
285         return _uiContext;
286     }
287 
288     ScriptPropertyDesc* getScriptProperty(int nthClass, int nthProp) nothrow
289     {
290         ScriptExportClass sec = _exportedClasses[nthClass];
291         ScriptPropertyDesc[] descs = sec.properties();
292         return &descs[nthProp];
293     }
294 
295 private:
296 
297     WrenVM* _vm = null;
298     IUIContext _uiContext;
299     double _timeSinceLastScriptCheck = 0; // in seconds
300     IdentifierBloom _identifierBloom;
301 
302     static struct PreloadedSource
303     {
304         char* moduleName;
305         char* source;
306     }
307 
308     static struct FileWatch
309     {
310     nothrow:
311     @nogc:
312         char* moduleName;
313         char* wrenFilePath;
314         char* lastSource;
315 
316         bool updateAndReturnIfChanged()
317         {
318             // FUTURE: eventually use stat to get date of change instead
319             char* newSource = readWrenFile(); 
320             
321             // Sometimes reading the file fails, and then we should just retry later. Report no changes.
322             if (newSource is null)
323                 return false;
324 
325             if ((lastSource is null) || strcmp(lastSource, newSource) != 0)
326             {
327                 free(lastSource);
328                 lastSource = newSource;
329                 return true;
330             }
331             else
332             {
333                 free(newSource);
334                 return false;
335             }
336         }
337 
338         char* readWrenFile()
339         {
340             ubyte[] content = readFile(wrenFilePath);
341             return cast(char*) content.ptr; // correct because readFile return one trailing '\0'
342         }
343     }
344 
345     /// All known premade modules.
346     Vec!PreloadedSource _preloadedSources;
347 
348     /// All known file-watch modules.
349     Vec!FileWatch _fileSources;
350 
351     /// All known D @ScriptExport classes.
352     Vec!ScriptExportClass _exportedClasses;
353 
354     /// "widgets" module source, recreated on import based upon _exportedClasses content.
355     Vec!char _widgetModuleSource;
356 
357     /// The number of time a Wren VM has been started. This is to invalidate caching of Wren values.
358     uint _vmGeneration = 0;
359 
360     /// Wren module, its look-up is cached to speed-up $ operator.
361     ObjModule* _cachedUIModule,
362                _cachedWidgetsModule;
363 
364     // ui.Element class, its look-up is cached to speed-up $ operator.
365     ObjClass* _cachedClassElement;
366 
367     void startWrenVM() nothrow
368     {
369         if (_vm !is null)
370             return; // Already started
371 
372         try
373         {
374             WrenConfiguration config;
375             wrenInitConfiguration(&config);
376 
377             config.writeFn             = &dplug_wrenPrint;
378             config.errorFn             = &dplug_wrenError;
379             config.dollarOperatorFn    = &dplug_wrenDollarOperator;
380             config.bindForeignMethodFn = &dplug_wrenBindForeignMethod;
381             config.bindForeignClassFn  = &dplug_wrenBindForeignClass;
382             config.loadModuleFn        = &dplug_wrenLoadModule;
383             config.userData            = cast(void*)this;
384 
385             // Note: wren defaults for memory usage make a lot of sense.
386             _vm = wrenNewVM(&config);
387             _vmGeneration++;
388             _cachedUIModule = null;
389             _cachedWidgetsModule = null;
390             _cachedClassElement = null;
391         }
392         catch(Exception e)
393         {
394             debugLog("VM initialization failed");
395             destroyFree(e);
396             _vm = null; 
397         }
398     }
399 
400     void stopWrenVM() nothrow
401     {
402         if (_vm !is null)
403         {
404             try
405             {
406                 wrenFreeVM(_vm);
407             }
408             catch(Exception e)
409             {
410                 destroyFree(e);
411             }
412             _vm = null;
413         }
414     }
415 
416     void print(const(char)* text) nothrow
417     {
418         debugLog(text);
419     }
420 
421     void error(WrenErrorType type, const(char)* module_, int line, const(char)* message) nothrow
422     {
423         switch(type)
424         {
425             case WrenErrorType.WREN_ERROR_COMPILE:
426                 debugLogf("%s(%d): Error: %s\n", module_, line, message);
427                 break;
428 
429             case WrenErrorType.WREN_ERROR_RUNTIME:
430                 debugLogf("wren crash: %s\n", message);
431                 break;
432 
433             case WrenErrorType.WREN_ERROR_STACK_TRACE:
434                 debugLogf("  %s.%s:%d\n", module_, message, line);
435                 break;
436 
437             default:
438                 assert(false);
439         }
440     }
441 
442     // this is called anytime Wren looks for a foreign method
443     WrenForeignMethodFn foreignMethod(WrenVM* vm, const(char)* module_, const(char)* className, bool isStatic, const(char)* signature) nothrow
444     {
445         // Introducing the Dplug-Wren standard library here.
446         try
447         {
448             if (strcmp(module_, "ui") == 0)
449             {
450                 return wrenUIBindForeignMethod(vm, className, isStatic, signature);
451             }
452             return null;
453         }
454         catch(Exception e)
455         {
456             destroyFree(e);
457             return null;
458         }
459     }
460 
461     // this is called anytime Wren looks for a foreign class
462     WrenForeignClassMethods foreignClass(WrenVM* vm, const(char)* module_, const(char)* className) nothrow
463     {
464         if (strcmp(module_, "ui") == 0)
465             return wrenUIForeignClass(vm, className);
466 
467         WrenForeignClassMethods methods;
468         methods.allocate = null;
469         methods.finalize = null;
470         return methods;
471     }
472 
473     WrenLoadModuleResult loadModule(WrenVM* vm, const(char)* name) nothrow
474     {
475         WrenLoadModuleResult res;
476         res.source = null;
477         res.onComplete = null;
478         res.userData = null;
479 
480         // Try file-watch source first
481         foreach(fw; _fileSources[])
482         {
483             if (strcmp(name, fw.moduleName) == 0)
484             {
485                 assert(fw.lastSource); // should have parsed the file preventively
486                 res.source = fw.lastSource;
487                 goto found;
488             }
489         }
490 
491         // Try preloaded source first
492         foreach(ps; _preloadedSources[])
493         {
494             if (strcmp(name, ps.moduleName) == 0)
495             {
496                 res.source = ps.source;
497                 goto found;
498             }
499         }
500 
501 
502         try
503         {   
504             if (strcmp(name, "widgets") == 0)
505                 res.source = widgetModuleSource();
506 
507             if (strcmp(name, "ui") == 0)
508                 res.source = wrenUIModuleSource();
509         }
510         catch(Exception e)
511         {
512             destroyFree(e);
513             res.source = null;
514         }
515         found:
516         return res;
517     }
518 
519     // Note: return a value whose lifetime is tied to Wren VM.
520     ObjModule* getWrenModule(const(char)* name)
521     {
522         Value moduleName = wrenStringFormat(_vm, "$", name);
523         wrenPushRoot(_vm, AS_OBJ(moduleName));
524         ObjModule* wModule = getModule(_vm, moduleName);
525         wrenPopRoot(_vm);
526         return wModule;
527     }
528 
529     ObjClass* getWrenClassForThisUIELement(UIElement elem)
530     {
531         // Do we have a valid cached ObjClass* inside the UIElement?
532         void* cachedClass = elem.getUserPointer(UIELEMENT_POINTERID_WREN_EXPORTED_CLASS);
533         if (cachedClass !is null)
534         {
535             // Same Wren VM?
536             uint cacheGen = cast(uint) elem.getUserPointer(UIELEMENT_POINTERID_WREN_VM_GENERATION);
537             if (cacheGen == _vmGeneration)
538             {
539                 // yes, reuse
540                 return cast(ObjClass*) cachedClass; 
541             }
542         }
543 
544         enum UIELEMENT_POINTERID_WREN_EXPORTED_CLASS = 0; /// The cached Wren class of this UIElement.
545         enum UIELEMENT_POINTERID_WREN_VM_GENERATION  = 1; /// The Wren VM count, as it is restarted. Stored as void*, but is an uint.
546 
547         // try to find concrete class directly
548         ObjClass* classTarget;
549         ScriptExportClass concreteClassInfo = findExportedClassByClassInfo(elem.classinfo);
550         if (concreteClassInfo)
551         {
552             const(char)* wrenClassName = concreteClassInfo.wrenClassNameZ();
553             classTarget = AS_CLASS(wrenFindVariable(_vm, _cachedWidgetsModule, wrenClassName));
554         }
555         else
556         {
557             // $ return a UIElement, no classes were found
558             // This is a silent error, properties assignment will silently fail.
559             // We still cache that incorrect value, as the real fix is registering the class to Wren.
560             classTarget = AS_CLASS(wrenFindVariable(_vm, _cachedUIModule, "UIElement"));
561         }
562 
563         // Cached return value inside the UIElement, which will speed-up future $
564         elem.setUserPointer(UIELEMENT_POINTERID_WREN_VM_GENERATION, classTarget);
565         elem.setUserPointer(UIELEMENT_POINTERID_WREN_VM_GENERATION, cast(void*) _vmGeneration);
566         return classTarget;
567     }
568 
569     // Implementation of the $ operator.
570     bool dollarOperator(WrenVM* vm, Value* args) nothrow
571     {
572         try
573         {
574             if (!IS_STRING(args[0]))
575                 return RETURN_NULL(args);
576 
577             const(char)* id = AS_STRING(args[0]).value.ptr;
578 
579             // Note: elem can be null here. If no element is found with a proper ID,
580             // return an unbound value.
581             UIElement elem = _uiContext.getElementById(id);
582             if (elem is null)
583                 return RETURN_NULL(args); // $(id) not found
584 
585             // Find a Wren class we have to convert it to.
586             // $ can be any of the imported classes in "widgets", if not it is an UIElement.
587             // Note that both "ui" and "widgets" module MUST be imported.
588 
589             if (_cachedUIModule is null)
590             {
591                 _cachedUIModule = getWrenModule("ui");
592                 if (_cachedUIModule is null)
593                     return RETURN_ERROR(vm, "module \"ui\" is not imported");
594             }
595 
596             if (_cachedWidgetsModule is null)
597             {
598                 _cachedWidgetsModule = getWrenModule("widgets");
599                 if (_cachedWidgetsModule is null)
600                     return RETURN_ERROR(vm, "module \"widgets\" is not imported");
601             }
602 
603             ObjClass* classTarget = getWrenClassForThisUIELement(elem);
604 
605             if (_cachedClassElement is null)
606             {
607                 _cachedClassElement = AS_CLASS(wrenFindVariable(vm, _cachedUIModule, "Element"));
608             }
609 
610             if (classTarget is null)
611             {
612                 return RETURN_ERROR(vm, "cannot create a IUElement from operator $");
613             }
614 
615             Value obj = wrenNewInstance(vm, classTarget);
616 
617             // PERF: could this be also cached? It wouldn't be managed by Wren.
618             // Create new Element foreign
619             ObjForeign* foreign = wrenNewForeign(vm, _cachedClassElement, UIElementBridge.sizeof);
620             UIElementBridge* bridge = cast(UIElementBridge*) foreign.data.ptr;
621             bridge.elem = elem; 
622 
623             // Assign it in the first field of the newly created ui.UIElement
624             ObjInstance* instance = AS_INSTANCE(obj);
625             instance.fields[0] = OBJ_VAL(foreign);
626 
627             return RETURN_OBJ(args, AS_INSTANCE(obj));
628         }
629         catch(Exception e)
630         {
631             destroyFree(e);
632             return RETURN_NULL(args); // other error
633         }
634     }
635 
636     // auto-generate the "widgets" Wren module
637     const(char)* widgetModuleSource() nothrow
638     {
639         _widgetModuleSource.clearContents();
640 
641         void text(const(char)[] s)
642         {
643             _widgetModuleSource.pushBack(cast(char[])s); // const_cast here
644         }
645 
646         void textZ(const(char)* s)
647         {
648             _widgetModuleSource.pushBack(cast(char[])s[0..strlen(s)]); // const_cast here
649         }
650 
651         void LF()
652         {
653             _widgetModuleSource.pushBack('\n');
654         }
655 
656         text(`import "ui" for UIElement, RGBA`); LF;
657 
658         foreach(size_t nthClass, ec; _exportedClasses[])
659         {
660             text("class "); textZ(ec.wrenClassNameZ); text(" is UIElement {"); LF;
661 
662             char[16] bufC;
663             snprintf(bufC.ptr, 16, "%d", cast(int)nthClass);
664 
665             foreach(size_t nth, prop; ec.properties())
666             {
667                 char[16] buf;
668                 snprintf(buf.ptr, 16, "%d", cast(int)nth);
669 
670                 bool isRGBA = prop.type == ScriptPropertyType.RGBA;
671 
672                 if (isRGBA)
673                 {
674                     // getter
675                     // PERF: this calls the wren function 4 times
676                     text("  "); text(prop.identifier); text("{"); LF;
677                     text("    return RGBA.new( e.getPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",0),");
678                                          text("e.getPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",1),");
679                                          text("e.getPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",2),");
680                                          text("e.getPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",3))"); LF;
681                     text("  }"); LF;
682 
683                     // setter for a RGBA property
684                     text("  "); text(prop.identifier); text("=(c){"); LF;
685                     text("    e.setPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",c.r, c.g, c.b, c.a)"); LF;
686                     text("  }"); LF;
687 
688                     // same but return this for chaining syntax
689                     text("  "); text(prop.identifier); text("(c){"); LF;
690                     text("    e.setPropRGBA_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",c.r, c.g, c.b, c.a)"); LF;
691                     text("    return this"); LF;
692                     text("  }"); LF;
693                 }
694                 else
695                 {
696                     // getter
697                     text("  "); text(prop.identifier); text("{"); LF;
698                     text("    return e.getProp_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(")"); LF;
699                     text("  }"); LF;
700 
701                     // setter for property (itself a Wren property setter)
702                     text("  "); text(prop.identifier); text("=(x){"); LF;
703                     text("    e.setProp_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",x)"); LF;
704                     text("  }"); LF;
705 
706                      // same but return this for chaining syntax
707                     text("  "); text(prop.identifier); text("(x){"); LF;
708                     text("    e.setProp_("); textZ(bufC.ptr); text(","); textZ(buf.ptr); text(",x)"); LF;
709                     text("    return this"); LF;
710                     text("  }"); LF;
711                 }
712             }
713             LF;
714             text("}"); LF; LF;
715         }
716 
717         _widgetModuleSource.pushBack('\0');
718         return _widgetModuleSource.ptr;
719     }
720 
721     ScriptExportClass findExportedClassByClassInfo(TypeInfo_Class info) nothrow
722     {
723         // PERF: again, not a simple lookup bt O(num-classes)
724         foreach(ScriptExportClass sec; _exportedClasses[])
725         {
726             if (sec.concreteClassInfo is info)
727             {
728                 // Found
729                 return sec;
730             }
731         }
732         return null;
733     }
734 
735     // Add a single UIElement derivative class into the set of known classes in Wren
736     // Note that it enumerates all @ScriptProperty from its ancestors too, so it works.
737     // Wren code doesn't actually know that UIImageKnob is derived from UIKnob.
738     private void registerDClass(alias aClass)(ScriptExportClass classDesc) nothrow
739     {
740         foreach(memberName; __traits(allMembers, aClass))
741         {
742             alias P = __traits(getMember, aClass, memberName);
743             foreach(attr; __traits(getAttributes, P))
744             {{
745                 static if(is(attr == ScriptProperty))
746                 {
747                     alias FieldType = typeof(P);
748 
749                     enum string fieldName = P.stringof;
750                     enum size_t offsetInClass = P.offsetof;
751 
752                     ScriptPropertyDesc desc;
753                     desc.identifier = fieldName;
754                     desc.offset = offsetInClass;
755                     static if (is(FieldType == enum))
756                     {
757                         // Note: enum are just integers in Wren, no translation of enum value.
758                         static if (FieldType.sizeof == 1)
759                             desc.type = ScriptPropertyType.byte_;
760                         else static if (FieldType.sizeof == 2)
761                             desc.type = ScriptPropertyType.short_;
762                         else static if (FieldType.sizeof == 4)
763                             desc.type = ScriptPropertyType.int_;
764                         else
765                             static assert(false, "Unsupported enum size in @ScriptProperty field " ~ fieldName ~  " of type " ~ FieldType.stringof);
766                     }
767                     else static if (is(FieldType == bool))
768                         desc.type = ScriptPropertyType.bool_;
769                     else static if (is(FieldType == RGBA))
770                         desc.type = ScriptPropertyType.RGBA;
771                     else static if (is(FieldType == ubyte))
772                         desc.type = ScriptPropertyType.ubyte_;
773                     else static if (is(FieldType == byte))
774                         desc.type = ScriptPropertyType.byte_;
775                     else static if (is(FieldType == ushort))
776                         desc.type = ScriptPropertyType.ushort_;
777                     else static if (is(FieldType == short))
778                         desc.type = ScriptPropertyType.short_;
779                     else static if (is(FieldType == uint))
780                         desc.type = ScriptPropertyType.uint_;
781                     else static if (is(FieldType == int))
782                         desc.type = ScriptPropertyType.int_;
783                     else static if (is(FieldType == float))
784                         desc.type = ScriptPropertyType.float_;
785                     else static if (is(FieldType == double))
786                         desc.type = ScriptPropertyType.double_;
787                     else static if (is(FieldType == L16)) // Note: this is temporary. TODO L16 properties should be eventually replaced by ushort instead.
788                         desc.type = ScriptPropertyType.ushort_;
789                     else
790                         static assert(false, "No @ScriptProperty support for field " ~ fieldName ~  " of type " ~ FieldType.stringof); // FUTURE: a way to add other types for properties?
791 
792                     classDesc.addProperty(desc);
793                 }
794             }}
795         }
796     }
797 }
798 
799 private:
800 
801 
802 void dplug_wrenPrint(WrenVM* vm, const(char)* text)
803 {
804     WrenSupport ws = cast(WrenSupport) vm.config.userData;
805     ws.print(text);
806 }
807 
808 void dplug_wrenError(WrenVM* vm, WrenErrorType type, const(char)* module_, int line, const(char)* message)
809 {
810     WrenSupport ws = cast(WrenSupport) vm.config.userData;
811     ws.error(type, module_, line, message);
812 }
813 
814 WrenForeignMethodFn dplug_wrenBindForeignMethod(WrenVM* vm, const(char)* module_, const(char)* className, bool isStatic, const(char)* signature)
815 {
816     WrenSupport ws = cast(WrenSupport) vm.config.userData;
817     return ws.foreignMethod(vm, module_, className, isStatic, signature);
818 }
819 
820 WrenForeignClassMethods dplug_wrenBindForeignClass(WrenVM* vm, const(char)* module_, const(char)* className)
821 {
822     WrenSupport ws = cast(WrenSupport) vm.config.userData;
823     return ws.foreignClass(vm, module_, className);
824 }
825 
826 WrenLoadModuleResult dplug_wrenLoadModule(WrenVM* vm, const(char)* name)
827 {
828     WrenSupport ws = cast(WrenSupport) vm.config.userData;
829     return ws.loadModule(vm, name);
830 }
831 
832 bool dplug_wrenDollarOperator(WrenVM* vm, Value* args)
833 {
834     WrenSupport ws = cast(WrenSupport) vm.config.userData;
835     return ws.dollarOperator(vm, args);
836 }
837 
838 
839 // Bloom filter to filter out if an identifier is not defined, quickly.
840 // Using the 64-bit FNV-1 hash: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
841 struct IdentifierBloom
842 {
843 pure nothrow @nogc @safe:
844     alias HashResult = ulong;
845 
846     ulong bits = 0;
847 
848     static HashResult hashOf(const(char)[] identifier)
849     {
850         ulong hash = 0xcbf29ce484222325;
851 
852         foreach(char ch; identifier)
853         {
854             hash = hash * 0x00000100000001B3;
855             hash = hash ^ cast(ulong)(ch);
856         }
857 
858         return hash;
859     }
860 
861     bool couldContain(HashResult identifierHash)
862     {
863         return (_bits & identifierHash) == identifierHash;
864     }
865 
866     void add(HashResult identifierHash)
867     {
868         _bits |= identifierHash;
869     }
870 
871 private:
872     ulong _bits = 0;
873 }