/* * Copyright (C)2005-2019 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ package php; import haxe.PosInfos; import haxe.extern.EitherType; using php.Global; /** Various Haxe->PHP compatibility utilities. You should not use this class directly. **/ @:keep @:dox(hide) class Boot { /** List of Haxe classes registered by their PHP class names */ @:protected static var aliases = new NativeAssocArray(); /** Cache of HxClass instances */ @:protected static var classes = new NativeAssocArray(); /** List of getters (for Reflect) */ @:protected static var getters = new NativeAssocArray>(); /** List of setters (for Reflect) */ @:protected static var setters = new NativeAssocArray>(); /** Metadata storage */ @:protected static var meta = new NativeAssocArray<{}>(); /** Cache for closures created of static methods */ @:protected static var staticClosures = new NativeAssocArray>(); /** Initialization stuff. This method is called once before invoking any Haxe-generated user code. **/ static function __init__() { Global.mb_internal_encoding('UTF-8'); if (!Global.defined('HAXE_CUSTOM_ERROR_HANDLER') || !Const.HAXE_CUSTOM_ERROR_HANDLER) { var previousLevel = Global.error_reporting(Const.E_ALL & ~Const.E_DEPRECATED); var previousHandler = Global.set_error_handler(function(errno:Int, errstr:String, errfile:String, errline:Int) { if (Global.error_reporting() & errno == 0) { return false; } /* * Division by zero should not throw * @see https://github.com/HaxeFoundation/haxe/issues/7034#issuecomment-394264544 */ if (errno == Const.E_WARNING && errstr == 'Division by zero') { return true; } throw new ErrorException(errstr, 0, errno, errfile, errline); }); // Already had user-defined handler. Return it. if (previousHandler != null) { Global.error_reporting(previousLevel); Global.set_error_handler(previousHandler); } } } /** Returns root namespace based on a value of `-D php-prefix=value` compiler flag. Returns empty string if no `-D php-prefix=value` provided. **/ public static function getPrefix():String { return Syntax.code('self::PHP_PREFIX'); } /** Register list of getters to be able to call getters using reflection **/ public static function registerGetters(phpClassName:String, list:NativeAssocArray):Void { getters[phpClassName] = list; } /** Register list of setters to be able to call getters using reflection **/ public static function registerSetters(phpClassName:String, list:NativeAssocArray):Void { setters[phpClassName] = list; } /** Check if specified property has getter **/ public static function hasGetter(phpClassName:String, property:String):Bool { if(!ensureLoaded(phpClassName)) return false; var has = false; var phpClassName:haxe.extern.EitherType = phpClassName; do { has = Global.isset(getters[phpClassName][property]); phpClassName = Global.get_parent_class(phpClassName); } while (!has && phpClassName != false && Global.class_exists(phpClassName)); return has; } /** Check if specified property has setter **/ public static function hasSetter(phpClassName:String, property:String):Bool { if(!ensureLoaded(phpClassName)) return false; var has = false; var phpClassName:haxe.extern.EitherType = phpClassName; do { has = Global.isset(setters[phpClassName][property]); phpClassName = Global.get_parent_class(phpClassName); } while (!has && phpClassName != false && Global.class_exists(phpClassName)); return has; } /** Save metadata for specified class **/ public static function registerMeta(phpClassName:String, data:Dynamic):Void { meta[phpClassName] = data; } /** Retrieve metadata for specified class **/ public static function getMeta(phpClassName:String):Null { if(!ensureLoaded(phpClassName)) return null; return Global.isset(meta[phpClassName]) ? meta[phpClassName] : null; } /** Associate PHP class name with Haxe class name **/ public static function registerClass(phpClassName:String, haxeClassName:String):Void { aliases[phpClassName] = haxeClassName; } /** Returns a list of currently loaded haxe-generated classes. **/ public static function getRegisteredClasses():Array> { var result = []; Syntax.foreach(aliases, function(phpName, haxeName) { result.push(cast getClass(phpName)); }); return result; } /** Returns a list of phpName=>haxeName for currently loaded haxe-generated classes. **/ public static function getRegisteredAliases():NativeAssocArray { return aliases; } /** Get Class instance for PHP fully qualified class name (E.g. '\some\pack\MyClass') It's always the same instance for the same `phpClassName` **/ public static function getClass(phpClassName:String):HxClass { if (phpClassName.charAt(0) == '\\') { phpClassName = phpClassName.substr(1); } if (!Global.isset(classes[phpClassName])) { classes[phpClassName] = new HxClass(phpClassName); } return classes[phpClassName]; } /** Returns Class **/ public static inline function getHxAnon():HxClass { return cast HxAnon; } /** Check if provided value is an anonymous object **/ public static inline function isAnon(v:Any):Bool { return Std.isOfType(v, HxAnon); } /** Returns Class **/ public static inline function getHxClass():HxClass { return cast HxClass; } /** Returns either Haxe class name for specified `phpClassName` or (if no such Haxe class registered) `phpClassName`. **/ public static function getClassName(phpClassName:String):String { var hxClass = getClass(phpClassName); var name = getHaxeName(hxClass); return (name == null ? hxClass.phpClassName : name); } /** Returns original Haxe fully qualified class name for this type (if exists) **/ public static function getHaxeName(hxClass:HxClass):Null { switch (hxClass.phpClassName) { case 'Int': return 'Int'; case 'String': return 'String'; case 'Bool': return 'Bool'; case 'Float': return 'Float'; case 'Class': return 'Class'; case 'Enum': return 'Enum'; case 'Dynamic': return 'Dynamic'; case _: } inline function exists() return Global.isset(aliases[hxClass.phpClassName]); if (exists()) { return aliases[hxClass.phpClassName]; } else if (Global.class_exists(hxClass.phpClassName) && exists()) { return aliases[hxClass.phpClassName]; } else if (Global.interface_exists(hxClass.phpClassName) && exists()) { return aliases[hxClass.phpClassName]; } return null; } /** Find corresponding PHP class name. Returns `null` if specified class does not exist. **/ public static function getPhpName(haxeName:String):Null { var prefix = getPrefix(); var phpParts = (Global.strlen(prefix) == 0 ? [] : [prefix]); var haxeParts = haxeName.split('.'); for (part in haxeParts) { if (isPhpKeyword(part)) { part += '_hx'; } phpParts.push(part); } return phpParts.join('\\'); } /** Check if the value of `str` is a reserved keyword in PHP @see https://www.php.net/manual/en/reserved.keywords.php **/ @:pure(false) static public function isPhpKeyword(str:String):Bool { //The body of this method is generated by the compiler return false; } /** Unsafe cast to HxClosure **/ public static inline function castClosure(value:Dynamic):HxClosure { return value; } /** Unsafe cast to HxClass **/ public static inline function castClass(cls:Class):HxClass { return cast cls; } /** Unsafe cast to HxEnum **/ public static inline function castEnumValue(enm:EnumValue):HxEnum { return cast enm; } /** Returns `Class` for `HxClosure` **/ public static inline function closureHxClass():HxClass { return cast HxClosure; } /** Implementation for `cast(value, Class)` @throws haxe.ValueError if `value` cannot be casted to this type **/ public static function typedCast(hxClass:HxClass, value:Dynamic):Dynamic { if (value == null) return null; switch (hxClass.phpClassName) { case 'Int': if (Boot.isNumber(value)) { return Global.intval(value); } case 'Float': if (Boot.isNumber(value)) { return value.floatval(); } case 'Bool': if (value.is_bool()) { return value; } case 'String': if (value.is_string()) { return value; } case 'array': if (value.is_array()) { return value; } case _: if (value.is_object() && Std.isOfType(value, cast hxClass)) { return value; } } throw 'Cannot cast ' + Std.string(value) + ' to ' + getClassName(hxClass.phpClassName); } /** Returns string representation of `value` **/ public static function stringify(value:Dynamic, maxRecursion:Int = 10):String { if (maxRecursion <= 0) { return '<...>'; } if (value == null) { return 'null'; } if (value.is_string()) { return value; } if (value.is_int() || value.is_float()) { return Syntax.string(value); } if (value.is_bool()) { return value ? 'true' : 'false'; } if (value.is_array()) { var strings = Syntax.arrayDecl(); Syntax.foreach(value, function(key:Dynamic, item:Dynamic) { strings.push(Syntax.string(key) + ' => ' + stringify(item, maxRecursion - 1)); }); return '[' + Global.implode(', ', strings) + ']'; } if (value.is_object()) { if (Std.isOfType(value, Array)) { return inline stringifyNativeIndexedArray(value.arr, maxRecursion - 1); } if (Std.isOfType(value, HxEnum)) { var e:HxEnum = value; var result = e.tag; if (Global.count(e.params) > 0) { var strings = Global.array_map(function(item) return Boot.stringify(item, maxRecursion - 1), e.params); result += '(' + Global.implode(',', strings) + ')'; } return result; } if (value.method_exists('toString')) { return value.toString(); } if (value.method_exists('__toString')) { return value.__toString(); } if (Std.isOfType(value, StdClass)) { if (Global.isset(Syntax.field(value, 'toString')) && value.toString.is_callable()) { return value.toString(); } var result = new NativeIndexedArray(); var data = Global.get_object_vars(value); for (key in data.array_keys()) { result.array_push('$key : ' + stringify(data[key], maxRecursion - 1)); } return '{ ' + Global.implode(', ', result) + ' }'; } if (isFunction(value)) { return ''; } if (Std.isOfType(value, HxClass)) { return '[class ' + getClassName((value : HxClass).phpClassName) + ']'; } else { return '[object ' + getClassName(Global.get_class(value)) + ']'; } } throw "Unable to stringify value"; } static public function stringifyNativeIndexedArray(arr:NativeIndexedArray, maxRecursion:Int = 10):String { var strings = Syntax.arrayDecl(); Syntax.foreach(arr, function(index:Int, value:T) { strings[index] = Boot.stringify(value, maxRecursion - 1); }); return '[' + Global.implode(',', strings) + ']'; } static public inline function isNumber(value:Dynamic) { return value.is_int() || value.is_float(); } /** Check if specified values are equal **/ public static function equal(left:Dynamic, right:Dynamic):Bool { if (isNumber(left) && isNumber(right)) { return Syntax.equal(left, right); } if (Std.isOfType(left, HxClosure) && Std.isOfType(right, HxClosure)) { return (left : HxClosure).equals(right); } return Syntax.strictEqual(left, right); } /** Concat `left` and `right` if both are strings or string and null. Otherwise return sum of `left` and `right`. **/ public static function addOrConcat(left:Dynamic, right:Dynamic):Dynamic { if (left.is_string() || right.is_string()) { return (left : String) + (right : String); } return Syntax.add(left, right); } @:deprecated('php.Boot.is() is deprecated. Use php.Boot.isOfType() instead') public static inline function is(value:Dynamic, type:HxClass):Bool { return isOfType(value, type); } /** `Std.isOfType()` implementation **/ public static function isOfType(value:Dynamic, type:HxClass):Bool { if (type == null) return false; var phpType = type.phpClassName; #if php_prefix var prefix = getPrefix(); if (Global.substr(phpType, 0, Global.strlen(prefix) + 1) == '$prefix\\') { phpType = Global.substr(phpType, Global.strlen(prefix) + 1); } #end switch (phpType) { case 'Dynamic': return value != null; case 'Int': return (value.is_int() || (value.is_float() && Syntax.equal(Syntax.int(value), value) && !Global.is_nan(value))) && Global.abs(value) <= 2147483648; case 'Float': return value.is_float() || value.is_int(); case 'Bool': return value.is_bool(); case 'String': return value.is_string(); case 'php\\NativeArray', 'php\\_NativeArray\\NativeArray_Impl_': return value.is_array(); case 'Enum' | 'Class': if (Std.isOfType(value, HxClass)) { var valuePhpClass = (cast value : HxClass).phpClassName; var enumPhpClass = (cast HxEnum : HxClass).phpClassName; var isEnumType = Global.is_subclass_of(valuePhpClass, enumPhpClass); return (phpType == 'Enum' ? isEnumType : !isEnumType); } case _: if (value.is_object()) { var type:Class = cast type; return Syntax.instanceof(value, type); } } return false; } /** Check if `value` is a `Class` **/ public static inline function isClass(value:Dynamic):Bool { return Std.isOfType(value, HxClass); } /** Check if `value` is an enum constructor instance **/ public static inline function isEnumValue(value:Dynamic):Bool { return Std.isOfType(value, HxEnum); } /** Check if `value` is a function **/ public static inline function isFunction(value:Dynamic):Bool { return Std.isOfType(value, Closure) || Std.isOfType(value, HxClosure); } /** Check if `value` is an instance of `HxClosure` **/ public static inline function isHxClosure(value:Dynamic):Bool { return Std.isOfType(value, HxClosure); } /** Performs `left >>> right` operation **/ public static function shiftRightUnsigned(left:Int, right:Int):Int { if (right == 0) { return left; } else if (left >= 0) { return (left >> right) & ~((1 << (8 * Const.PHP_INT_SIZE - 1)) >> (right - 1)); } else { return (left >> right) & (0x7fffffff >> (right - 1)); } } /** Helper method to avoid "Cannot use temporary expression in write context" error for expressions like this: ```haxe (new MyClass()).fieldName = 'value'; ``` **/ static public function deref(value:Dynamic):Dynamic { return value; } /** Create Haxe-compatible anonymous structure of `data` associative array **/ static public function createAnon(data:NativeArray):Dynamic { var o = new HxAnon(); Syntax.foreach(data, (field:String, value:Any) -> Syntax.setField(o, field, value)); return o; } /** Make sure specified class is loaded **/ static public inline function ensureLoaded(phpClassName:String):Bool { return Global.class_exists(phpClassName) || Global.interface_exists(phpClassName); } /** Get `field` of a dynamic `value` in a safe manner (avoid exceptions on trying to get a method) **/ static public function dynamicField(value:Dynamic, field:String):Dynamic { if (Global.method_exists(value, field)) { return closure(value, field); } if (Global.is_string(value)) { value = @:privateAccess new HxDynamicStr(value); } return Syntax.field(value, field); } public static function dynamicString(str:String):HxDynamicStr { return @:privateAccess new HxDynamicStr(str); } /** Creates Haxe-compatible closure of an instance method. @param obj - any object **/ public static function getInstanceClosure(obj:{?__hx_closureCache:NativeAssocArray}, methodName:String):Null { var result = Syntax.coalesce(obj.__hx_closureCache[methodName], null); if (result != null) { return result; } if(!Global.method_exists(obj, methodName) && !Global.isset(Syntax.field(obj, methodName))) { return null; } result = new HxClosure(obj, methodName); if (!Global.property_exists(obj, '__hx_closureCache')) { obj.__hx_closureCache = new NativeAssocArray(); } obj.__hx_closureCache[methodName] = result; return result; } /** Creates Haxe-compatible closure of a static method. **/ public static function getStaticClosure(phpClassName:String, methodName:String) { var result = Syntax.coalesce(staticClosures[phpClassName][methodName], null); if (result != null) { return result; } result = new HxClosure(phpClassName, methodName); if (!Global.array_key_exists(phpClassName, staticClosures)) { staticClosures[phpClassName] = new NativeAssocArray(); } staticClosures[phpClassName][methodName] = result; return result; } /** Creates Haxe-compatible closure. @param type `this` for instance methods; full php class name for static methods @param func Method name **/ public static inline function closure(target:Dynamic, func:String):HxClosure { return target.is_string() ? getStaticClosure(target, func) : getInstanceClosure(target, func); } /** Get UTF-8 code of the first character in `s` without any checks **/ static public inline function unsafeOrd(s:NativeString):Int { var code = Global.ord(s[0]); if (code < 0xC0) { return code; } else if (code < 0xE0) { return ((code - 0xC0) << 6) + Global.ord(s[1]) - 0x80; } else if (code < 0xF0) { return ((code - 0xE0) << 12) + ((Global.ord(s[1]) - 0x80) << 6) + Global.ord(s[2]) - 0x80; } else { return ((code - 0xF0) << 18) + ((Global.ord(s[1]) - 0x80) << 12) + ((Global.ord(s[2]) - 0x80) << 6) + Global.ord(s[3]) - 0x80; } } static public function divByZero(value:Float):Float { return value == 0 ? Const.NAN : (value < 0 ? -Const.INF : Const.INF); } } /** Class implementation for Haxe->PHP internals. **/ @:keep @:dox(hide) private class HxClass { public var phpClassName(default, null):String; public function new(phpClassName:String):Void { this.phpClassName = phpClassName; } /** Magic method to call static methods of this class, when `HxClass` instance is in a `Dynamic` variable. **/ @:phpMagic function __call(method:String, args:NativeArray):Dynamic { var callback = (phpClassName == 'String' ? Syntax.nativeClassName(HxString) : phpClassName) + '::' + method; return Global.call_user_func_array(callback, args); } /** Magic method to get static vars of this class, when `HxClass` instance is in a `Dynamic` variable. **/ @:phpMagic function __get(property:String):Dynamic { if (Global.defined('$phpClassName::$property')) { return Global.constant('$phpClassName::$property'); } else if (Boot.hasGetter(phpClassName, property)) { return Syntax.staticCall(phpClassName, 'get_$property'); } else if (phpClassName.method_exists(property)) { return Boot.getStaticClosure(phpClassName, property); } else { return Syntax.getStaticField(phpClassName, property); } } /** Magic method to set static vars of this class, when `HxClass` instance is in a `Dynamic` variable. **/ @:phpMagic function __set(property:String, value:Dynamic):Void { if (Boot.hasSetter(phpClassName, property)) { Syntax.staticCall(phpClassName, 'set_$property', value); } else { Syntax.setStaticField(phpClassName, property, value); } } } /** Base class for enum types **/ @:keep @:dox(hide) @:allow(php.Boot.stringify) @:allow(Type) private class HxEnum { final tag:String; final index:Int; final params:NativeArray; public function new(tag:String, index:Int, arguments:NativeArray = null):Void { this.tag = tag; this.index = index; params = (arguments == null ? new NativeArray() : arguments); } /** Get string representation of this `Class` **/ public function toString():String { return __toString(); } /** PHP magic method to get string representation of this `Class` **/ @:phpMagic public function __toString():String { return Boot.stringify(this); } extern public static function __hx__list():Array; } /** `String` implementation **/ @:keep @:dox(hide) private class HxString { public static function toUpperCase(str:String):String { return Global.mb_strtoupper(str); } public static function toLowerCase(str:String):String { return Global.mb_strtolower(str); } public static function charAt(str:String, index:Int):String { return index < 0 ? '' : Global.mb_substr(str, index, 1); } public static function charCodeAt(str:String, index:Int):Null { if (index < 0 || str == '') { return null; } if (index == 0) { return Boot.unsafeOrd(str); } var char = Global.mb_substr(str, index, 1); return char == '' ? null : Boot.unsafeOrd(char); } public static function indexOf(str:String, search:String, startIndex:Int = null):Int { if (startIndex == null) { startIndex = 0; } else { var length = str.length; if (startIndex < 0) { startIndex += length; if (startIndex < 0) { startIndex = 0; } } if (startIndex >= length && search != '') { return -1; } } var index:EitherType = if (search == '') { var length = str.length; startIndex > length ? length : startIndex; } else { Global.mb_strpos(str, search, startIndex); } return (index == false ? -1 : index); } public static function lastIndexOf(str:String, search:String, startIndex:Int = null):Int { var start = startIndex; if (start == null) { start = 0; } else { var length = str.length; if (start >= 0) { start = start - length; if (start > 0) { start = 0; } } else if (start < -length) { start = -length; } } var index:EitherType = if (search == '') { var length = str.length; startIndex == null || startIndex > length ? length : startIndex; } else { Global.mb_strrpos(str, search, start); } if (index == false) { return -1; } else { return index; } } public static function split(str:String, delimiter:String):Array { var arr:NativeArray = if (delimiter == '') { Global.preg_split('//u', str, -1, Const.PREG_SPLIT_NO_EMPTY); } else { delimiter = Global.preg_quote(delimiter, '/'); Global.preg_split('/$delimiter/', str); } return @:privateAccess Array.wrap(arr); } public static function substr(str:String, pos:Int, ?len:Int):String { return Global.mb_substr(str, pos, len); } public static function substring(str:String, startIndex:Int, ?endIndex:Int):String { if (endIndex == null) { if (startIndex < 0) { startIndex = 0; } return Global.mb_substr(str, startIndex); } if (endIndex < 0) { endIndex = 0; } if (startIndex < 0) { startIndex = 0; } if (startIndex > endIndex) { var tmp = endIndex; endIndex = startIndex; startIndex = tmp; } return Global.mb_substr(str, startIndex, endIndex - startIndex); } public static function toString(str:String):String { return str; } public static function fromCharCode(code:Int):String { return Global.mb_chr(code); } } /** For Dynamic access which looks like String. Instances of this class should not be saved anywhere. Instead it should be used to immediately invoke a String field right after instance creation one time only. **/ @:dox(hide) @:keep private class HxDynamicStr extends HxClosure { static var hxString:String = (cast HxString : HxClass).phpClassName; /** Returns HxDynamicStr instance if `value` is a string. Otherwise returns `value` as-is. **/ static function wrap(value:Dynamic):Dynamic { if (value.is_string()) { return new HxDynamicStr(value); } else { return value; } } static inline function invoke(str:String, method:String, args:NativeArray):Dynamic { Global.array_unshift(args, str); return Global.call_user_func_array(hxString + '::' + method, args); } function new(str:String) { super(str, null); } @:phpMagic function __get(field:String):Dynamic { switch (field) { case 'length': return (target : String).length; case _: func = field; return this; } } @:phpMagic function __call(method:String, args:NativeArray):Dynamic { return invoke(target, method, args); } /** @see http://php.net/manual/en/language.oop5.magic.php#object.invoke **/ @:phpMagic override public function __invoke() { return invoke(target, func, Global.func_get_args()); } /** Generates callable value for PHP **/ override public function getCallback(eThis:Dynamic = null):NativeIndexedArray { if (eThis == null) { return Syntax.arrayDecl((this : Dynamic), func); } return Syntax.arrayDecl((new HxDynamicStr(eThis) : Dynamic), func); } /** Invoke this closure with `newThis` instead of `this` **/ override public function callWith(newThis:Dynamic, args:NativeArray):Dynamic { if (newThis == null) { newThis = target; } return invoke(newThis, func, args); } } /** Anonymous objects implementation **/ @:keep @:dox(hide) private class HxAnon extends StdClass { @:phpMagic function __get(name:String) { return null; } @:phpMagic function __call(name:String, args:NativeArray):Dynamic { return Syntax.code("($this->{0})(...{1})", name, args); } } /** Closures implementation **/ @:keep @:dox(hide) private class HxClosure { /** `this` for instance methods; php class name for static methods */ var target:Dynamic; /** Method name for methods */ var func:String; /** A callable value, which can be invoked by PHP */ var callable:Any; public function new(target:Dynamic, func:String):Void { this.target = target; this.func = func; // Force runtime error if trying to create a closure of an instance which happen to be `null` if (target.is_null()) { throw "Unable to create closure on `null`"; } callable = Std.isOfType(target, HxAnon) ? Syntax.field(target, func) : Syntax.arrayDecl(target, func); } /** @see http://php.net/manual/en/language.oop5.magic.php#object.invoke **/ @:phpMagic public function __invoke() { return Global.call_user_func_array(callable, Global.func_get_args()); } /** Generates callable value for PHP **/ public function getCallback(eThis:Dynamic = null):NativeIndexedArray { if (eThis == null) { eThis = target; } if (Std.isOfType(eThis, HxAnon)) { return Syntax.field(eThis, func); } return Syntax.arrayDecl(eThis, func); } /** Check if this is the same closure **/ public function equals(closure:HxClosure):Bool { return (target == closure.target && func == closure.func); } /** Invoke this closure with `newThis` instead of `this` **/ public function callWith(newThis:Dynamic, args:NativeArray):Dynamic { return Global.call_user_func_array(getCallback(newThis), args); } }