1 /**
2     D high level binding for Duktape.
3 
4     It add automatic registration of D symbol.
5 */
6 module duktape;
7 
8 import std.stdio;
9 import etc.c.duktape;
10 import std..string : toStringz, fromStringz;
11 import std.traits;
12 
13 enum AllMembers(alias Symbol) = __traits(allMembers, Symbol);
14 enum Protection(alias Symbol) = __traits(getProtection, Symbol);
15 enum Identifier(alias Symbol) = __traits(identifier, Symbol);
16 
17 static bool IsPublic(alias Symbol)() { return Protection!Symbol == "public"; }
18 
19 
20 class DukContextException : Exception
21 {
22     this(string msg, string file = __FILE__, size_t line = __LINE__) {
23         super(msg, file, line);
24     }
25 }
26 
27 
28 /** Advanced duk context.
29 
30 It allow to register D symbol directly.
31 */
32 final class DukContext
33 {
34     import std.conv : to;
35     import std.typecons;
36 
37 private:
38     duk_context *_ctx;
39     static immutable char* CLASS_DATA_PROP ; /// "\xFF" mean to hide property
40     static immutable char* CLASS_DELETED_PROP;
41     @property duk_context* raw() { return _ctx; }
42 
43 public:
44     static this()
45     {
46         CLASS_DATA_PROP = ("\xFF\xFF" ~ "objPtr").toStringz();
47         CLASS_DELETED_PROP = ("\xFF\xFF" ~ "objDel").toStringz();
48     }
49 
50     this()
51     {
52         //_ctx = duk_create_heap_default;
53         _ctx = duk_create_heap(null, null, null, null, &my_fatal);
54     }
55 
56     extern (C) static void my_fatal(void *udata, const char *msg)
57     {
58         /* Note that 'msg' may be NULL. */
59         throw new DukContextException(fromStringz(msg).to!string);
60     }
61 
62     ~this()
63     {
64         duk_destroy_heap(_ctx);
65     }
66 
67     /** Evaluate a JS string and get an optional result
68         Params:
69             js = the source code
70     */
71     T evalString(T=void)(string js)
72     {
73         duk_eval_string(_ctx, js.toStringz());
74 
75         static if (!is(T == void))
76             return get!T();
77     }
78 
79     ///
80     unittest
81     {
82         auto ctx = new DukContext();
83         ctx.evalString("a = 42;");
84         assert(ctx.evalString!int("a = 1 + 2") == 3);
85     }
86 
87     /** Register a global object in JS context. */
88     DukContext registerGlobal(alias Symbol)(string name = Identifier!Symbol)
89     {
90         register!Symbol();
91         duk_put_global_string(_ctx, name.toStringz());
92         return this;
93     }
94 
95     /// Set a previously registered symbol as global.
96     DukContext setGlobal(string name)
97     {
98         duk_put_global_string(_ctx, name.toStringz());
99         return this;
100     }
101 
102     ///
103     unittest
104     {
105         static int add(int a, int b) {
106             return a + b;
107         }
108 
109         auto ctx = new DukContext();
110         ctx.registerGlobal!add;
111 
112         auto res = ctx.evalString!int("add(1, 5)");
113         assert(res == 6);
114     }
115 
116     /// Automatic registration of D function. (not global)
117     DukContext register(alias Func)() if (isFunction!Func)
118     {
119         auto externFunc = generateExternDukFunc!Func;
120         duk_push_c_function(_ctx, externFunc, Parameters!Func.length /*nargs*/);
121         return this;
122     }
123 
124     ///
125     unittest
126     {
127         static int square(int n) { return n*n; }
128 
129         auto ctx = new DukContext();
130         ctx.register!square.setGlobal("square"); // equivalent to ctx.registerGlobal!square
131         assert(ctx.evalString!int(r"a = square(5)") == 25);
132     }
133 
134     /// Automatic registration of D enum. (not global)
135     DukContext register(alias Enum)() if (is(Enum == enum))
136     {
137         alias EnumBaseType = OriginalType!Enum;
138 
139         duk_idx_t arr_idx;
140         arr_idx = duk_push_array(_ctx);
141 
142         // push a js array
143         static foreach(Member; EnumMembers!Enum) {
144             this.push!EnumBaseType(_ctx, cast(EnumBaseType) Member); // push value
145             duk_put_prop_string(_ctx, arr_idx, to!string(Member).toStringz()); // push string prop
146         }
147         return this;
148     }
149 
150     ///
151     unittest
152     {
153         enum Direction { up = 0, down = 1 }
154 
155         auto ctx = new DukContext();
156         ctx.register!Direction.setGlobal("Direction"); // equivalent to ctx.registerGlobal!Direction
157         assert(ctx.evalString!int(r"a = Direction.down") == 1);
158     }
159 
160     /// Automatic registration of D class. (not global)
161     DukContext register(alias Class)() if (is(Class == class))
162     {
163         import std.algorithm: canFind;
164 
165         enum MemberToIgnore = [
166             "__ctor", "__dtor", "this",
167             "__xdtor", "toHash", "opCmp",
168             "opEquals", "Monitor", "factory",
169         ];
170         enum Members = AllMembers!Class;
171 
172         // create constructor function
173         auto dukContructor = this.generateExternDukConstructor!Class;
174         duk_push_c_function(_ctx, dukContructor,
175             Parameters!(__traits(getMember, Class.init, "__ctor")).length);
176 
177         /* Push MyObject.prototype object. */
178         int objIdx = duk_push_object(_ctx);
179 
180         Class base;
181         uint propFlags = 0;
182         // push prototype methods
183         static foreach(Method; Members) {
184             // Error: class foo.Foo member x is not accessible workaround
185             static if (is(typeof(__traits(getMember, Class.init, Method)))) {
186                 static if (IsPublic!(__traits(getMember, base, Method)) && !MemberToIgnore.canFind(Method)) {
187                     static if (isFunction!(__traits(getMember, base, Method))) {
188                         // it is a property and exclude toString
189                         static if (hasFunctionAttributes!(__traits(getMember, Class.init, Method), "@property") &&
190                                 (Method.stringof !is "toString")) {
191                             // iterate property overloads
192                             push!string(Identifier!(__traits(getMember, Class.init, Method)));  // [... key]
193                             propFlags = DUK_DEFPROP_FORCE | DUK_DEFPROP_HAVE_CONFIGURABLE;
194 
195                             // the getter must be registered first
196                             static foreach(GetterSetter; __traits(getOverloads, Class, Method)) {
197                                 // its a getter
198                                 static if (Parameters!GetterSetter.length is 0) {
199                                     duk_push_c_function(_ctx,
200                                         generateExternDukMethod!(Class, GetterSetter),
201                                         Parameters!(GetterSetter).length ); // [obj key get]
202                                     propFlags |= DUK_DEFPROP_HAVE_GETTER;
203                                 }
204                             }
205 
206                             // try to register a setter
207                             static foreach(GetterSetter; __traits(getOverloads, Class, Method)) {
208                                 // its a setter
209                                 static if (Parameters!GetterSetter.length !is 0) {
210                                     duk_push_c_function(_ctx,
211                                         generateExternDukMethod!(Class, GetterSetter),
212                                         Parameters!(GetterSetter).length ); // [obj key get]
213                                     propFlags |= DUK_DEFPROP_HAVE_SETTER;
214                                 }
215                             }
216                             duk_def_prop(_ctx, objIdx, propFlags); // [obj]
217                         }
218                         else {
219                             duk_push_c_function(_ctx,
220                                 generateExternDukMethod!(Class, __traits(getMember, Class.init, Method)),
221                                 Parameters!(__traits(getMember, Class.init, Method)).length /*nargs*/); // [obj func]
222                             duk_put_prop_string(_ctx, objIdx, Method); // [obj func]
223                         }
224                     }
225                 }
226             }
227         }
228 
229          /* Set MyObject.prototype = proto */
230         duk_put_prop_string(_ctx, objIdx - 1, "prototype");
231 
232         return this;
233     }
234 
235     ///
236     unittest
237     {
238         // Point is a class that hold x, y coordinates
239         auto ctx = new DukContext();
240         ctx.register!Point.setGlobal("Point"); // equivalent to ctx.registerGlobal!Point
241         assert(ctx.evalString!string(r"new Point(1, 2).toString()") == "(1, 2)");
242     }
243 
244     /** Open a new JS namespace.
245     You can then register symbol inside and call finalize() when
246     its done.
247     */
248     NamespaceContext createNamespace(string name)
249     {
250         return new NamespaceContext(this, name);
251     }
252 
253     ///
254     unittest
255     {
256         enum Direction { up, down }
257 
258         auto ctx = new DukContext();
259 
260         ctx.createNamespace("Com")
261             .register!Direction
262             .finalize();
263 
264         assert(ctx.evalString!int("Com.Direction.down") == 1);
265     }
266 
267 
268     /// Get a value on the stack.
269     T get(T)(int idx = -1)
270     {
271         return get!T(_ctx, idx);
272     }
273 
274     ///
275     unittest
276     {
277         auto ctx = new DukContext();
278 
279         ctx.push([1, 2, 3]);
280 
281         assert([1, 2, 3] == ctx.get!(int[]));
282     }
283 
284     void push(T)(T value)
285     {
286         return push!T(_ctx, value);
287     }
288 
289 private:
290     /// Utility method to push a type on the stack.
291     static void push(T)(duk_context *ctx, T value)
292     {
293         static if (is(T == int))         duk_push_int(ctx, value);
294         else static if (is(T == bool))   duk_push_boolean(ctx, value);
295         else static if (is(T == float))  duk_push_number(ctx, value);
296         else static if (is(T == double)) duk_push_number(ctx, value);
297         else static if (is(T == string)) duk_push_string(ctx, value.toStringz());
298         else static if (is(T == enum))   push!(OriginalType!T)(ctx, cast(OriginalType!T) value);
299         else static if (is(T == class)) {
300             // Store the underlying object
301             duk_push_pointer(ctx, cast(void*) value);
302             duk_put_prop_string(ctx, -2, CLASS_DATA_PROP);
303 
304             // Store a boolean flag to mark the object as deleted because the destructor may be called several times
305             duk_push_boolean(ctx, false);
306             duk_put_prop_string(ctx, -2, CLASS_DELETED_PROP);
307 
308         }
309         else static if (isArray!T) {
310             alias Elem = ForeachType!T;
311             auto arrIdx = duk_push_array(ctx);
312 
313             foreach(uint i, Elem elem; value) {
314                 push!Elem(ctx, elem);
315                 duk_put_prop_index(ctx, arrIdx, i);
316             }
317         }
318         else {
319             static assert(false, T.stringof ~ " argument is not handled.");
320         }
321     }
322 
323     /// Utility method to get a type on the stack.
324     static T get(T)(duk_context *ctx, int idx = -1)
325     {
326         static if (is(T == int))    return duk_require_int(ctx, idx);
327         else static if (is(T == bool))   return duk_require_boolean(ctx, idx);
328         else static if (is(T == float))  return duk_require_number(ctx, idx);
329         else static if (is(T == double)) return duk_require_number(ctx, idx);
330         else static if (is(T == string)) return fromStringz(duk_require_string(ctx, idx)).to!string;
331         else static if (is(T == enum))   return cast(T) get!(OriginalType!T)(ctx, idx); // get enum base type
332         else static if (is(T == class)) {
333             if (!duk_is_object(ctx, idx))
334                 duk_error(ctx, DUK_ERR_TYPE_ERROR, "expected an object");
335 
336             duk_get_prop_string(ctx, idx, CLASS_DATA_PROP);
337             void* addr = duk_get_pointer(ctx, -1);
338             duk_pop(ctx);  // pop CLASS_DATA_PROP
339             return cast(T) addr;
340         }
341         else static if (isArray!T) {
342             if (!duk_is_array(ctx, idx))
343                 duk_error(ctx, DUK_ERR_TYPE_ERROR, "expected an array of " ~ ForeachType!T.stringof);
344 
345             T result;
346             ulong length = duk_get_length(ctx, idx);
347             for (int i = 0; i < length; i++) {
348                 duk_get_prop_index(ctx, idx, i);
349                 result ~= get!(ForeachType!T)(ctx, -1); // recursion on array element type
350                 duk_pop(ctx); // duk_get_prop_index
351             }
352 
353             return result;
354         }
355         else static if (is(T == delegate)) {
356             // build a delegate englobing duk call
357             return (Parameters!T args) {
358                 duk_require_function(ctx, idx); // [... func ...]
359                 duk_dup(ctx, idx); // [... func ... func]
360 
361                 // prepare for a duk_call
362                 static foreach(i, PT; Parameters!T)
363                     push!PT(ctx, args[i]); // [... func ... func arg1 argN ...]
364 
365                 duk_call(ctx, Parameters!T.length); // [... func ... func retval]
366                 static if (is(ReturnType!T == void))
367                     duk_pop_2(ctx);
368                 else {
369                     auto result = get!(ReturnType!T)(ctx, -1);
370                     duk_pop_2(ctx);
371                     return result;
372                 }
373             };
374         }
375         else {
376             static assert(false, T.stringof ~ " argument is not handled.");
377         }
378     }
379 
380     /** Get all function arguments on the stask.
381     Params:
382         ctx = duk context
383     Template_Params:
384         Func = the func
385     Returns: A tuple of arguments.
386     */
387     static auto getArgs(alias Func)(duk_context *ctx) if (isFunction!Func)
388     {
389         Tuple!(Parameters!Func) args;
390         static foreach(i, ArgType; Parameters!Func) {
391             args[i] = get!ArgType(ctx, i);
392         }
393         return args;
394     }
395 
396     /** Call the function with a tuple of arguments.
397         Returns: the number of return valuee
398     */
399     static int call(alias Func)(duk_context *ctx, Tuple!(Parameters!Func) args) if (isFunction!Func)
400     {
401         static if (is(ReturnType!Func == void)) {
402             Func(args.expand);
403             return 0;
404         }
405         else {
406             ReturnType!Func ret = Func(args.expand);
407             push(ctx, ret);
408             return 1; // one return value
409         }
410     }
411 
412     /** Call the method with a tuple of arguments.
413         Returns: the number of return valuee
414     */
415     static int callMethod(alias Method, T)(duk_context *ctx, Tuple!(Parameters!Method) args, T instance) if (isFunction!Method)
416     {
417         static if (is(ReturnType!Method == void)) {
418             __traits(getMember, instance, Identifier!Method)(args.expand);
419             return 0;
420         }
421         else {
422             ReturnType!Method ret = __traits(getMember, instance, Identifier!Method)(args.expand);
423             push(ctx, ret);
424             return 1; // one return value
425         }
426     }
427 
428     auto generateExternDukFunc(alias Func)() if (isFunction!Func)
429     {
430         extern(C) static duk_ret_t func(duk_context *ctx) {
431             int n = duk_get_top(ctx);  // [arg1 argN ...]
432             // check parameter count
433             if (n != Parameters!Func.length)
434                 return DUK_RET_RANGE_ERROR;
435 
436             auto args = getArgs!Func(ctx);
437             return call!Func(ctx, args);
438         }
439 
440         return &func;
441     }
442 
443     auto generateExternDukMethod(alias Class, alias Method)() if (is(Class == class) && isFunction!Method)
444     {
445         import std.typecons;
446 
447         extern(C) static duk_ret_t func(duk_context *ctx) {
448             duk_push_this(ctx); // [this]
449             duk_get_prop_string(ctx, -1, CLASS_DATA_PROP); // [this val]
450             void* addr = duk_get_pointer(ctx, -1);
451             duk_pop_2(ctx); // []
452             Class instance = cast(Class) addr;
453 
454             int n = duk_get_top(ctx);  // number of args
455 
456             // check parameter count
457             if (n != Parameters!Method.length)
458                 return DUK_RET_RANGE_ERROR;
459 
460             auto args = getArgs!Method(ctx);
461             duk_pop_n(ctx, n);
462             return callMethod!Method(ctx, args, instance);
463         }
464 
465         return &func;
466     }
467 
468     auto generateExternDukConstructor(alias Class)() if (is(Class == class))
469     {
470         import std.typecons;
471 
472         extern(C) static duk_ret_t func(duk_context *ctx) {
473             if (!duk_is_constructor_call(ctx)) {
474                 return DUK_RET_TYPE_ERROR;
475             }
476 
477             // must have a constructor
478             static assert(hasMember!(Class, "__ctor"), Class.stringof ~ ": a constructor is required.");
479 
480             // check constructor parameter count
481             int n = duk_get_top(ctx);  // [arg1 argn ...]
482             if (n != Parameters!(__traits(getMember, Class.init, "__ctor")).length)
483                 return DUK_RET_RANGE_ERROR;
484 
485             auto args = getArgs!(__traits(getMember, Class.init, "__ctor"))(ctx);
486             duk_pop_n(ctx, n);
487 
488             // Push special this binding to the function being constructed
489             duk_push_this(ctx); // [this]
490 
491             // instanciate class @nogc
492             // lifetime is managed by j
493             auto instance = new Class(args.expand);
494 
495             // Store the underlying object
496             duk_push_pointer(ctx, cast(void*) instance); // [this ptr]
497             duk_put_prop_string(ctx, -2, CLASS_DATA_PROP); // [this]
498 
499             // Store a boolean flag to mark the object as deleted because the destructor may be called several times
500             duk_push_boolean(ctx, false); // [this bool]
501             duk_put_prop_string(ctx, -2, CLASS_DELETED_PROP); // [this]
502 
503             auto classDestructor = generateExternDukDestructor!Class(ctx);
504 
505             // Store the function destructor
506             duk_push_c_function(ctx, classDestructor, 1); // [this func]
507             duk_set_finalizer(ctx, -2); // [this]
508 
509             duk_pop(ctx); // pop this
510 
511             return 0;
512         }
513 
514         return &func;
515     }
516 
517     static auto generateExternDukDestructor(alias Class)(duk_context *ctx) if (is(Class == class))
518     {
519         import std.typecons;
520 
521         extern(C) static duk_ret_t func(duk_context *ctx) {
522             // The object to delete is passed as first argument instead
523             duk_get_prop_string(ctx, 0, CLASS_DELETED_PROP); // [obj val]
524 
525             bool deleted = (duk_to_boolean(ctx, -1) != 0);
526             duk_pop(ctx); // [obj]
527 
528             if (!deleted) {
529                 auto str = CLASS_DATA_PROP;
530 
531                 duk_get_prop_string(ctx, 0, CLASS_DATA_PROP); // [obj val]
532                 void* addr = duk_to_pointer(ctx, -1); // [obj val]
533                 duk_pop(ctx); // [obj]
534 
535                 Class instance = cast(Class) addr;
536                 destroy(instance);
537 
538                 // Mark as deleted
539                 duk_push_boolean(ctx, true); // [obj bool]
540                 duk_put_prop_string(ctx, 0, CLASS_DELETED_PROP); // [obj]
541             }
542 
543             duk_pop(ctx);
544 
545             return 0;
546         }
547 
548         return &func;
549     }
550 }
551 
552 ///
553 unittest
554 {
555     static Point add(Point a, Point b) {
556         return new Point(a.x + b.x, a.y + b.y);
557     }
558 
559     enum Directions { up, down }
560 
561     auto ctx = new DukContext();
562     ctx.registerGlobal!add;
563     ctx.registerGlobal!Directions;
564     ctx.registerGlobal!Point;
565 
566 
567     assert(ctx.evalString!string(q"{
568         p1 = new Point(20, 40);
569         p2 = new Point(10, 20);
570         p3 = add(p1, p2);
571 
572         p3.toString();
573     }") == "(30, 60)");
574 }
575 
576 /// Namespace support
577 final class NamespaceContext
578 {
579 private:
580     DukContext _ctx;
581     string _name;
582     duk_idx_t _arrIdx;
583     bool _finalized = false;
584 
585 public:
586     this(DukContext ctx, string name)
587     {
588         _ctx = ctx;
589         _name = name;
590 
591         // a namespace is a js array
592         _arrIdx = duk_push_array(_ctx.raw);
593     }
594 
595     ~this()
596     {
597         if (!_finalized)
598             finalize();
599     }
600 
601     NamespaceContext register(alias Symbol)(string name = Identifier!Symbol)
602     {
603         _ctx.register!Symbol();
604         duk_put_prop_string(_ctx.raw, _arrIdx, name.toStringz()); // push string prop
605         return this;
606     }
607 
608     void finalize()
609     {
610         duk_put_global_string(_ctx.raw, _name.toStringz());
611         _finalized = true;
612     }
613 }
614 
615 ///
616 unittest
617 {
618     enum Direction
619     {
620         up,
621         down
622     }
623 
624     auto ctx = new DukContext();
625 
626     ctx.createNamespace("Work")
627         .register!Direction
628         .finalize();
629 
630     auto res = ctx.evalString!int("Work.Direction.down");
631     assert(res == 1);
632 }
633 
634 version (unittest)
635 {
636     class Foo {}
637 
638     class Point
639     {
640         float _x;
641         float _y;
642 
643         @property float x() { return _x; }
644         @property void x(float v) { _x = v; }
645         @property float y() { return _y; }
646         @property void y(float v) { _y = v; }
647 
648         this(float x, float y)
649         {
650             this._x = x;
651             this._y = y;
652         }
653 
654         ~this()
655         {
656         }
657 
658         override string toString()
659         {
660             import std.conv : to;
661             return "(" ~ to!string(x) ~ ", " ~ to!string(y) ~ ")";
662         }
663     }
664 }
665 
666 // Class: must have a constructor
667 unittest
668 {
669     auto ctx = new DukContext();
670     assert(!__traits(compiles, ctx.registerGlobal!Foo));
671 }
672 
673 //
674 unittest
675 {
676     static string capitalize(string s) {
677         import std..string : capitalize;
678         return s.capitalize();
679     }
680 
681     auto ctx = new DukContext();
682     ctx.registerGlobal!capitalize;
683 
684     auto res = ctx.evalString!string(`capitalize("hEllO")`);
685     assert(res == "Hello");
686 }
687 
688 // register!Enum
689 unittest
690 {
691     enum Direction
692     {
693         up,
694         down
695     }
696 
697     auto ctx = new DukContext();
698     ctx.registerGlobal!Direction;
699 
700     auto res = ctx.evalString!int("Direction['up']");
701     assert(res == 0);
702 
703     res = ctx.evalString!int("Direction['down']");
704     assert(res == 1);
705 }
706 
707 // class
708 unittest
709 {
710     static void inc(Point p) {
711         p.x = p.x + 1;
712         p.y = p.y + 1;
713     }
714 
715     static void incArray(Point[] pts) {
716         foreach(p; pts) inc(p);
717     }
718 
719     auto ctx = new DukContext();
720     ctx.registerGlobal!Point;
721     ctx.registerGlobal!inc;
722     ctx.registerGlobal!incArray;
723 
724     auto res = ctx.evalString!string(q"{
725         p1 = new Point(20, 40);
726         p2 = new Point(10, 20);
727         p2.toString();
728         inc(p2);
729         p2.toString();
730     }");
731     assert(res == "(11, 21)");
732 
733     res = ctx.evalString!string(q"{
734         arr = [new Point(0, 1), new Point(2, 3)];
735         incArray(arr);
736         arr[1].toString();
737     }");
738     assert(res == "(3, 4)");
739 }
740 
741 // class properties
742 unittest
743 {
744 
745     auto ctx = new DukContext();
746     ctx.registerGlobal!Point;
747 
748     auto res = ctx.evalString!int(q"{
749         p = new Point(45, 96);
750         p.x = 12;
751         p.y = 26
752         a = p.x + p.y
753     }");
754     assert(res == 12 + 26);
755 }
756 
757 // arrays
758 unittest
759 {
760     static T[] sort(T)(T[] arr) {
761         import std.algorithm.sorting : sort;
762         return arr.sort().release();
763     }
764 
765     auto ctx = new DukContext();
766     ctx.registerGlobal!(sort!int);
767 
768     auto res = ctx.evalString!(int[])("sort([5, 1, 3])");
769     assert(res == [1, 3, 5]);
770 }
771 
772 // callable arguments
773 unittest
774 {
775     alias Callable = int delegate(int, int);
776     static int callWith(Callable callable, int a1, int a2) {
777         return callable(a1, a2);
778     }
779 
780     auto ctx = new DukContext();
781     ctx.registerGlobal!callWith;
782 
783     auto res = ctx.evalString!int(q"{
784         callWith(function(a, b) {return a+b; }, 1, 2);
785     }");
786     assert(res == 3);
787 }