diff --git a/haxelib.json b/haxelib.json index 458fe34..c24d402 100755 --- a/haxelib.json +++ b/haxelib.json @@ -16,6 +16,7 @@ "classPath": "src/main", "dependencies": { "promhx": "1.1.0", + "protohx": "0.4.6", "yaml": "2.0.0" } } diff --git a/src/main/hw/connect/BaseConnection.hx b/src/main/hw/connect/BaseConnection.hx new file mode 100755 index 0000000..3c5d13d --- /dev/null +++ b/src/main/hw/connect/BaseConnection.hx @@ -0,0 +1,52 @@ +package hw.connect; + +import haxe.io.Bytes; +import hw.connect.IConnection; +import hw.signal.Signal; +import promhx.Deferred; +import promhx.Promise; +import protohx.Message; + +class BaseConnection implements IConnection { + public var handler(default, null):Signal; + public var sendHandler(default, null):Signal; + public var receiveHandler(default, null):Signal; + public var connected(default, null):Bool; + public var queue(default, null):PacketQueue; + + private var connectDeferred:Deferred>; + + public function new(inputFactory:Class) { + queue = new PacketQueue(inputFactory); + handler = new Signal(); + sendHandler = new Signal(); + receiveHandler = new Signal(); + } + + public function connect():Promise> { + throw "Not implemented"; + } + + public function disconnect():Void { + throw "Not implemented"; + } + + public function pushData(bytes:Bytes):Void { + #if proto_debug L.d('Proto', 'pushData: ${bytes.length}'); #end + queue.addBytes(bytes); + while (queue.hasMsg()) { + var packet:I = queue.popMsg(); + receive(packet); + } + } + + public function send(packet:O):Void { + #if proto_debug L.d('Proto', 'send: ${packet}'); #end + sendHandler.emit(packet); + } + + public function receive(packet:I):Void { + #if proto_debug L.d('Proto', 'receive: ${packet}'); #end + receiveHandler.emit(packet); + } +} diff --git a/src/main/hw/connect/ConnectionFactory.hx b/src/main/hw/connect/ConnectionFactory.hx new file mode 100644 index 0000000..147f2cd --- /dev/null +++ b/src/main/hw/connect/ConnectionFactory.hx @@ -0,0 +1,16 @@ +package hw.connect; + +import protohx.Message; + +class ConnectionFactory { + + public static function buildClientConnection(host: String, port:Int, inputFactory:Class):IConnection { + #if flash + return new hw.connect.flash.FlashConnection(host, port, inputFactory); + #elseif html5 + return new hw.connect.js.JsConnection(host, port + (hw.connect.js.JsConnection.isSecured() ? 1 : 0), inputFactory); + #else + return new hw.connect.desktop.DesktopConnection(host, port, inputFactory); + #end + } +} diff --git a/src/main/hw/connect/IConnection.hx b/src/main/hw/connect/IConnection.hx new file mode 100755 index 0000000..da2cda5 --- /dev/null +++ b/src/main/hw/connect/IConnection.hx @@ -0,0 +1,24 @@ +package hw.connect; + +import haxe.io.Bytes; +import hw.signal.Signal; +import promhx.Promise; +import protohx.Message; + +enum ConnectionEvent { + CONNECTED; + DISCONNECTED; + ERROR(error:Dynamic); +} + +interface IConnection { + public var connected(default, null):Bool; + public var handler(default, null):Signal; + public var sendHandler(default, null):Signal; + public var receiveHandler(default, null):Signal; + + public function connect():Promise>; + public function disconnect():Void; + public function send(packet:O):Void; + public function pushData(bytes:Bytes):Void; +} diff --git a/src/main/hw/connect/PacketQueue.hx b/src/main/hw/connect/PacketQueue.hx new file mode 100755 index 0000000..d27cbfa --- /dev/null +++ b/src/main/hw/connect/PacketQueue.hx @@ -0,0 +1,70 @@ +package hw.connect; + +import haxe.io.Bytes; +import haxe.io.BytesBuffer; +import haxe.io.BytesInput; +import protohx.Message; + +class PacketQueue { + + public var packetClass(default, null):Class

; + + private var buffer:BytesBuffer; + private var msgs:List

; + + public function new(packetClass:Class

) { + this.packetClass = packetClass; + msgs = new List

(); + buffer = new BytesBuffer(); + } + + public inline function hasMsg():Bool { + return !msgs.isEmpty(); + } + + public inline function popMsg():P { + return msgs.pop(); + } + + public inline function addMsg(msg:P):Void { + msgs.add(msg); + } + + private function readPackage():Null

{ + var bytes = buffer.getBytes(); + var input = new BytesInput(bytes); + input.bigEndian = false; + if (input.length > 1) { + var packetSize = input.readUInt16(); + if (input.length >= packetSize + 2) { + var packet:P = Type.createInstance(packetClass, []); + try { + packet.mergeFrom(input.read(packetSize)); + buffer = new BytesBuffer(); + buffer.add(input.read(input.length - (packetSize + 2))); + return packet; + } catch (error:Dynamic) { + L.w("PacketQueue", "readPackage ", error); + buffer = new BytesBuffer(); + return null; + } + } + } + buffer = new BytesBuffer(); + buffer.add(bytes); + return null; + } + + public function addBytes(bytes:Bytes):Void { + buffer.add(bytes); + var packet = readPackage(); + while (packet != null) { + msgs.add(packet); + packet = readPackage(); + } + } + + public function clean():Void { + buffer = new BytesBuffer(); + } +} diff --git a/src/main/hw/connect/PacketUtil.hx b/src/main/hw/connect/PacketUtil.hx new file mode 100644 index 0000000..dde687f --- /dev/null +++ b/src/main/hw/connect/PacketUtil.hx @@ -0,0 +1,20 @@ +package hw.connect; + +import haxe.io.Bytes; +import haxe.io.BytesOutput; +import protohx.Message; + +class PacketUtil { + + public static function fromBytes(bytes:Bytes, factory:Class

):P { + var packet:P = Type.createInstance(factory, []); + packet.mergeFrom(bytes); + return packet; + } + + public static function toBytes(packet:P):Bytes { + var out = new BytesOutput(); + packet.writeTo(out); + return out.getBytes(); + } +} diff --git a/src/main/hw/connect/WebSocketTools.hx b/src/main/hw/connect/WebSocketTools.hx new file mode 100644 index 0000000..601576a --- /dev/null +++ b/src/main/hw/connect/WebSocketTools.hx @@ -0,0 +1,24 @@ +package hw.connect; + +import haxe.io.BytesOutput; + + +class WebSocketTools { + + public static function packet2string(packet:Message):String { + var b = new BytesOutput(); + packet.writeTo(b); + var data = b.getBytes(); + var res = new BytesOutput(); + //res.writeUInt16(data.length); + res.write(data); + return Base64.encodeBase64(res.getBytes()); + } + + public static function string2packet(data:String, packetClass:Class

):P { + var bytes = Base64.decodeBase64(data); + var packet:P = Type.createInstance(packetClass, []); + packet.mergeFrom(bytes); + return packet; + } +} diff --git a/src/main/hw/connect/desktop/DesktopConnection.hx b/src/main/hw/connect/desktop/DesktopConnection.hx new file mode 100644 index 0000000..1c706a0 --- /dev/null +++ b/src/main/hw/connect/desktop/DesktopConnection.hx @@ -0,0 +1,76 @@ +package hw.connect.desktop; + +import cpp.vm.Thread; +import haxe.Timer; +import hw.connect.IConnection; +import promhx.Deferred; +import promhx.Promise; +import protohx.Message; +import sys.net.Host; +import sys.net.Socket; + +class DesktopConnection extends BaseConnection { + + private var host:String; + private var port:Int; + private var socket:Socket; + private var reader:Thread; + + public function new(host:String, port:Int, inputFactory:Class) { + super(inputFactory); + this.host = host; + this.port = port; + connected = false; + socket = new Socket(); + socket.setFastSend(true); + socket.output.bigEndian = false; + socket.input.bigEndian = false; + sendHandler.connect(_send); + } + + override public function connect():Promise> { + connectDeferred = new Deferred(); + try { + if (connected) { + connectDeferred.resolve(this); + } else { + socket.connect(new Host(host), port); + connected = true; + reader = Thread.create(_read); + connectDeferred.resolve(this); + handler.emit(ConnectionEvent.CONNECTED); + } + } catch (error:Dynamic) { + handler.emit(ConnectionEvent.ERROR(error)); + Timer.delay(function() connectDeferred.throwError(error), 1); + } + return connectDeferred.promise(); + } + + override public function disconnect():Void { + socket.close(); + connected = false; + handler.emit(DISCONNECTED); + } + + private function _read():Void { + try { + while (connected) { + socket.waitForRead(); + var size = socket.input.readUInt16(); + var data = socket.input.read(size); + var packet:I = PacketUtil.fromBytes(data, queue.packetClass); + receiveHandler.emit(packet); + } + } catch (error:Dynamic) { + handler.emit(ERROR(error)); + } + } + + private function _send(packet:O):Void { + var bytes = PacketUtil.toBytes(packet); + socket.output.writeUInt16(bytes.length); + socket.output.write(bytes); + socket.output.flush(); + } +} diff --git a/src/main/hw/connect/fake/FakeConnection.hx b/src/main/hw/connect/fake/FakeConnection.hx new file mode 100644 index 0000000..945f043 --- /dev/null +++ b/src/main/hw/connect/fake/FakeConnection.hx @@ -0,0 +1,17 @@ +package hw.connect.fake; + +import promhx.Promise; +import protohx.Message; + +class FakeConnection extends BaseConnection { + + override public function connect():Promise> { + handler.emit(ConnectionEvent.CONNECTED); + var promise:Promise> = cast Promise.promise(this); + return promise; + } + + override public function disconnect():Void { + handler.emit(ConnectionEvent.DISCONNECTED); + } +} diff --git a/src/main/hw/connect/flash/FlashConnection.hx b/src/main/hw/connect/flash/FlashConnection.hx new file mode 100755 index 0000000..320775a --- /dev/null +++ b/src/main/hw/connect/flash/FlashConnection.hx @@ -0,0 +1,89 @@ +package hw.connect.flash; + +import flash.events.ErrorEvent; +import flash.events.Event; +import flash.events.IOErrorEvent; +import flash.events.ProgressEvent; +import flash.events.SecurityErrorEvent; +import flash.net.Socket; +import flash.utils.Endian; +import haxe.io.Bytes; +import promhx.Deferred; +import promhx.Promise; +import protohx.Message; +import hw.connect.IConnection; + +class FlashConnection extends BaseConnection { + + private var host:String; + private var port:Int; + private var socket:Socket; + + public function new(host:String, port:Int, inputFactory:Class) { + super(inputFactory); + this.host = host; + this.port = port; + connected = false; + socket = new Socket(); + socket.addEventListener(IOErrorEvent.IO_ERROR, onError); + socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError); + socket.addEventListener(Event.CLOSE, onClose); + socket.addEventListener(Event.CONNECT, onConnect); + socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData); + socket.endian = Endian.LITTLE_ENDIAN; + sendHandler.connect(_send); + } + + override public function connect():Promise> { + socket.connect(host, port); + connectDeferred = new Deferred(); + return connectDeferred.promise(); + } + + override public function disconnect():Void { + if (socket.connected) { + socket.close(); + connected = false; + handler.emit(ConnectionEvent.DISCONNECTED); + } + } + + private function onError(event:ErrorEvent):Void { + socket.close(); + connected = false; + handler.emit(ConnectionEvent.ERROR(event)); + if (connectDeferred != null) { + connectDeferred.throwError(event); + connectDeferred = null; + } + } + + private function onConnect(_):Void { + connected = true; + handler.emit(ConnectionEvent.CONNECTED); + if (connectDeferred != null) { + connectDeferred.resolve(this); + connectDeferred = null; + } + } + + private function onClose(_):Void { + socket.close(); + connected = false; + handler.emit(ConnectionEvent.DISCONNECTED); + } + + private function onSocketData(_):Void { + var data = new flash.utils.ByteArray(); + socket.readBytes(data); + var bytes = Bytes.ofData(data); + pushData(bytes); + } + + private function _send(packet:O):Void { + var bytes = PacketUtil.toBytes(packet); + socket.writeShort(bytes.length); + socket.writeBytes(bytes.getData()); + socket.flush(); + } +} diff --git a/src/main/hw/connect/js/JsConnection.hx b/src/main/hw/connect/js/JsConnection.hx new file mode 100644 index 0000000..d2dd598 --- /dev/null +++ b/src/main/hw/connect/js/JsConnection.hx @@ -0,0 +1,91 @@ +package hw.connect.js; + +import haxe.io.Bytes; +import hw.connect.IConnection; +import js.Browser; +import js.html.BinaryType; +import js.html.WebSocket; +import promhx.Deferred; +import promhx.Promise; +import protohx.Message; + +class JsConnection extends BaseConnection { + + private var host:String; + private var port:Int; + private var socket:WebSocket; + + public function new(host:String, port:Int, inputFactory:Class) { + super(inputFactory); + this.host = host; + this.port = port; + connected = false; + } + + public static function isSecured():Bool { + return Browser.location.protocol == "https:"; + } + + private function buildSocket(host:String, port:Int):WebSocket { + var protocol = isSecured() ? "wss:" : "ws:"; + return new WebSocket('$protocol//$host:$port'); + } + + override public function connect():Promise> { + var self = this; + socket = buildSocket(host, port); + socket.binaryType = BinaryType.ARRAYBUFFER; + socket.onopen = this.onConnect; + socket.onclose = this.onClose; + socket.onerror = this.onError; + socket.onmessage = this.onSocketData; + connectDeferred = new Deferred(); + return connectDeferred.promise(); + } + + override public function disconnect():Void { + socket.close(1000); + connected = false; + } + + private function onError(event:Dynamic):Void { + socket.close(1000); + connected = false; + handler.emit(ConnectionEvent.ERROR(event)); + } + + private function onConnect(_):Void { + connected = true; + handler.emit(ConnectionEvent.CONNECTED); + connectDeferred.resolve(this); + } + + private function onClose(_):Void { + socket.close(); + connected = false; + handler.emit(ConnectionEvent.DISCONNECTED); + } + + private function onSocketData(event:Dynamic):Void { + var packet:I = null; + try { + var bytes = Bytes.ofData(event.data); + packet = PacketUtil.fromBytes(bytes, queue.packetClass); + } catch (error:Dynamic) { + handler.emit(ConnectionEvent.ERROR(error)); + } + if (packet != null) { + receive(packet); + } + } + + override public function send(packet:O):Void { + if (connected) { + super.send(packet); + var bytes = PacketUtil.toBytes(packet); + socket.send(bytes.getData()); + } else { + L.w("Connection", "closed"); + } + } +} diff --git a/src/main/hw/connect/neko/NekoConnection.hx b/src/main/hw/connect/neko/NekoConnection.hx new file mode 100755 index 0000000..6be7dbb --- /dev/null +++ b/src/main/hw/connect/neko/NekoConnection.hx @@ -0,0 +1,57 @@ +package hw.connect.neko; + +import haxe.Timer; +import protohx.Message; +import sys.net.Socket; + +class NekoConnection extends BaseConnection { + + public var socket(default, null):Socket; + + private var sendQueue:Array; + private var timer:Timer; + + public function new(socket:Socket, i:Class) { + super(i); + this.socket = socket; + socket.setFastSend(true); + socket.output.bigEndian = false; + socket.input.bigEndian = false; + sendHandler.connect(pushPacket); + sendQueue = []; + timer = new Timer(1); + timer.run = sendRun; + } + + private function sendPacket(packet:O):Void { + try { + var bytes = PacketUtil.toBytes(packet); + socket.output.writeUInt16(bytes.length); + socket.output.write(bytes); + socket.output.flush(); + } catch (error:Dynamic) { + L.e('Proto', 'Error send packet: ${packet}', error); + } + } + + private function sendRun():Void { + if (sendQueue.length > 0) { + for (packet in sendQueue) { + sendPacket(packet); + } + sendQueue = []; + } + } + + private function pushPacket(packet:O):Void { + sendQueue.push(packet); + } + + override public function disconnect():Void { + if (timer != null) { + timer.stop(); + timer = null; + } + super.disconnect(); + } +} diff --git a/src/main/hw/connect/neko/NekoWSConnection.hx b/src/main/hw/connect/neko/NekoWSConnection.hx new file mode 100644 index 0000000..76885f1 --- /dev/null +++ b/src/main/hw/connect/neko/NekoWSConnection.hx @@ -0,0 +1,178 @@ +package hw.connect.neko; + +import haxe.crypto.BaseCode; +import haxe.crypto.Sha1; +import haxe.io.Bytes; +import haxe.io.BytesBuffer; +import protohx.Message; +import sys.net.Socket; + +class NekoWSConnection extends NekoConnection { + + private var opened:Bool; + + public function new(socket:Socket, i:Class) { + super(socket, i); + opened = false; + } + + override private function sendPacket(packet:O):Void { + var data = PacketUtil.toBytes(packet); + writeData(data, socket); + } + + override public function pushData(bytes:Bytes):Void { + if (!opened) { + var str:String = bytes.getString(0, bytes.length); + if (StringTools.startsWith(str, "GET")) { + var r = ~/Sec-WebSocket-Key:\s*([A-z0-9=+\/]+)/; + r.match(str); + opened = true; + sendServerHandShake(socket, r.matched(1)); + } + } else { + var data = parseData(bytes); + if (data != null) { + var packet:I = PacketUtil.fromBytes(data, queue.packetClass); + receive(packet); + } + } + } + + private function encodeBase64(content:String):String { + var suffix = switch (content.length % 3) + { + case 2: "="; + case 1: "=="; + default: ""; + }; + return BaseCode.encode(content, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + suffix; + } + + private function hex2data(hex:String):String { + var data = ""; + for (i in 0...Std.int(hex.length / 2)) { + data += String.fromCharCode(Std.parseInt("0x" + hex.substr(i * 2, 2))); + } + return data; + } + + private function sendServerHandShake(socket:sys.net.Socket, inpKey:String) { + var outKey = encodeBase64(hex2data(Sha1.encode(StringTools.trim(inpKey) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))); + + var s = "HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: " + outKey + "\r\n" + + "\r\n"; + + socket.output.writeString(s); + } + + private function writeData(data:Bytes, socket:sys.net.Socket, isServer = true):Void { + socket.output.writeByte(130); + + var len = 0; + if (data.length < 126) len = data.length; + else if (data.length < 65536) len = 126; + else len = 127; + + socket.output.writeByte(len | (!isServer ? 0x80 : 0x00)); + + if (data.length >= 126) { + if (data.length < 65536) { + socket.output.writeByte((data.length >> 8) & 0xFF); + socket.output.writeByte(data.length & 0xFF); + } + else { + socket.output.writeByte((data.length >> 24) & 0xFF); + socket.output.writeByte((data.length >> 16) & 0xFF); + socket.output.writeByte((data.length >> 8) & 0xFF); + socket.output.writeByte(data.length & 0xFF); + } + } + + if (isServer) { + socket.output.writeBytes(data, 0, data.length); + } + else { + var mask = [ Std.random(256), Std.random(256), Std.random(256), Std.random(256) ]; + socket.output.writeByte(mask[0]); + socket.output.writeByte(mask[1]); + socket.output.writeByte(mask[2]); + socket.output.writeByte(mask[3]); + var maskedData = new BytesBuffer(); + for (i in 0...data.length) { + maskedData.addByte(data.get(i) ^ mask[i % 4]); + } + socket.output.writeBytes(maskedData.getBytes(), 0, maskedData.length); + } + } + + private function parseData(bytes:Bytes):Bytes { + var p = 0; + var opcode = bytes.get(p++); + + if (opcode == 0x00) { + var data = new BytesBuffer(); + var b:Int; + while ((b = bytes.get(p++)) != 0xFF) { + data.addByte(b); + } + return data.getBytes(); + } + + // 130 = binary data + if (opcode == 130) { + var len = bytes.get(p++); + + // mask + if (len & 0x80 != 0) { + len &= 0x7F; + + if (len == 126) { + var b2 = bytes.get(p++); + var b3 = bytes.get(p++); + len = (b2 << 8) + b3; + } + else if (len == 127) { + var b2 = bytes.get(p++); + var b3 = bytes.get(p++); + var b4 = bytes.get(p++); + var b5 = bytes.get(p++); + len = (b2 << 24) + (b3 << 16) + (b4 << 8) + b5; + } + + //Lib.println("len = " + len); + + // direct array init not work corectly! + var mask = []; + mask.push(bytes.get(p++)); + mask.push(bytes.get(p++)); + mask.push(bytes.get(p++)); + mask.push(bytes.get(p++)); + + //Lib.println("mask = " + mask); + + var data = new BytesBuffer(); + for (i in 0...len) { + data.addByte(bytes.get(p++) ^ mask[i % 4]); + } + + //Lib.println("readed = " + data.toString()); + return data.getBytes(); + } else { + throw "Expected masked data."; + } + } + + if (opcode == 136) { + //socket.close(); + opened = false; + return null; + } else { + throw "Unsupported websocket opcode: " + opcode; + } + return null; + } +} diff --git a/src/main/hw/connect/session/ISession.hx b/src/main/hw/connect/session/ISession.hx new file mode 100644 index 0000000..d0d6a27 --- /dev/null +++ b/src/main/hw/connect/session/ISession.hx @@ -0,0 +1,9 @@ +package hw.connect.session; + +import haxe.io.Bytes; + +interface ISession { + public var id(default, null):Int; + public function pushData(bytes:Bytes):Void; + public function disconnect():Void; +} diff --git a/src/main/hw/connect/session/ProtoSession.hx b/src/main/hw/connect/session/ProtoSession.hx new file mode 100644 index 0000000..c64e200 --- /dev/null +++ b/src/main/hw/connect/session/ProtoSession.hx @@ -0,0 +1,71 @@ +package hw.connect.session; + +import haxe.io.Bytes; +import hw.connect.neko.NekoConnection; +import hw.connect.neko.NekoWSConnection; +import protohx.Message; +import sys.net.Socket; + +class ProtoSession implements ISession { + private static inline var TAG = "Session"; + + private static var POLICY_FILE:String = [ + "", + "", + "", + "", + "", + "" + ].join("\n"); + + private static var idCounter:Int = 0; + + public var id(default, null):Int; + public var connection(default, null):IConnection; + private var socket:Socket; + private var request:Class; + + public function new(socket:Socket, request:Class) { + this.id = ++idCounter; + this.socket = socket; + this.request = request; + } + + private function buildConnection(bytes:Bytes):IConnection { + var str:String = bytes.getString(0, bytes.length); + if (str == "" + String.fromCharCode(0)) { + L.d(TAG, "policy-file-request"); + socket.output.writeString(POLICY_FILE + String.fromCharCode(0)); + socket.output.flush(); + return null; + } + if (StringTools.startsWith(str, "GET")) { + connection = new NekoWSConnection(socket, request); + } else { + connection = new NekoConnection(socket, request); + } + connection.receiveHandler.connect(onRequest); + return connection; + } + + public function send(packet:O):Void { + connection.send(packet); + } + + public function pushData(bytes:Bytes):Void { + if (connection == null) { + connection = buildConnection(bytes); + } + if (connection != null) { + connection.pushData(bytes); + } + } + + public function disconnect():Void { + connection.disconnect(); + } + + private function onRequest(request:I):Void { + L.d(TAG, 'onRequest: ${request}'); + } +} diff --git a/src/main/hw/geom/Point.hx b/src/main/hw/geom/Point.hx index fa04f91..04a04e3 100644 --- a/src/main/hw/geom/Point.hx +++ b/src/main/hw/geom/Point.hx @@ -1,7 +1,5 @@ package hw.geom; -import flash.geom.Point as FlashPoint; - abstract Point(Array) { public var x(get, set):Float; public var y(get, set):Float; @@ -33,8 +31,4 @@ abstract Point(Array) { public function toString():String { return 'Point{x=$x,y=$y}'; } - - @:from public static function fromFlashPoint(value:FlashPoint):Point { - return new Point(value.x, value.y); - } } diff --git a/src/main/hw/log/TraceLogger.hx b/src/main/hw/log/TraceLogger.hx index 0009b23..18f304e 100755 --- a/src/main/hw/log/TraceLogger.hx +++ b/src/main/hw/log/TraceLogger.hx @@ -37,7 +37,7 @@ class TraceLogger extends BaseLogger { #elseif php untyped __call__('_hx_trace', v); #elseif cpp - Stdio.printf(ConstCharStar.fromString(Std.string(v))); + Stdio.printf(ConstCharStar.fromString(Std.string(v)+'\n')); //untyped __trace(v, null); #elseif cs cs.system.Console.WriteLine(v);