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 }