commit f8744a29e57d467583fdd0512ca8fd4cd3069a79
parent 2b58783da6e5023c2f2c6d30bb3c7bd3078ceb3f
Author: Michael Savage <mikejsavage@gmail.com>
Date:   Wed,  5 Sep 2018 20:47:11 +0300
Move event loop to C to more closely match how Windows works
This means sockets have to be created/polled on the C side, which means
lua-ev and luasocket are both gone.
Also made script debugging a bit easier.
Diffstat:
24 files changed, 907 insertions(+), 209 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,4 +1,4 @@
-mudGangster
+mudgangster
 build
 release
 gen.mk
diff --git a/Makefile b/Makefile
@@ -1,7 +1,7 @@
 all: debug
 .PHONY: debug asan release clean
 
-build/lua_bytecode.h: src/lua/action.lua src/lua/alias.lua src/lua/chat.lua src/lua/connect.lua src/lua/event.lua src/lua/gag.lua src/lua/handlers.lua src/lua/intercept.lua src/lua/interval.lua src/lua/macro.lua src/lua/main.lua src/lua/script.lua src/lua/serialize.lua src/lua/status.lua src/lua/sub.lua src/lua/utils.lua
+build/lua_bytecode.h: src/lua/action.lua src/lua/alias.lua src/lua/chat.lua src/lua/mud.lua src/lua/event.lua src/lua/gag.lua src/lua/handlers.lua src/lua/intercept.lua src/lua/interval.lua src/lua/macro.lua src/lua/main.lua src/lua/script.lua src/lua/serialize.lua src/lua/status.lua src/lua/sub.lua src/lua/utils.lua src/lua/socket.lua
 	@printf "\033[1;33mbuilding $@\033[0m\n"
 	@scripts/pack_lua.sh
 
diff --git a/make.lua b/make.lua
@@ -5,5 +5,5 @@ require( "scripts.gen_makefile" )
 -- require( "libs.lua" )
 -- require( "libs.lpeg" )
 
-bin( "mudgangster", { "src/main", "src/script", "src/textbox", "src/input", "src/x11" } )
+bin( "mudgangster", { "src/main", "src/script", "src/textbox", "src/input", "src/x11", "src/platform_network" } )
 gcc_bin_ldflags( "mudgangster", "-lm -lX11 -llua" ) -- -Wl,-E" ) need to export symbols when vendoring
diff --git a/scripts/gen_makefile.lua b/scripts/gen_makefile.lua
@@ -14,7 +14,7 @@ local configs = {
 
 		toolchain = "msvc",
 
-		cxxflags = "/I . /c /Oi /Gm- /GR- /EHa- /EHsc /nologo /DNOMINMAX /DWIN32_LEAN_AND_MEAN",
+		cxxflags = "/I src /c /Oi /Gm- /GR- /EHa- /EHsc /nologo /DNOMINMAX /DWIN32_LEAN_AND_MEAN",
 		ldflags = "user32.lib shell32.lib advapi32.lib dbghelp.lib /nologo",
 		warnings = "/W4 /wd4100 /wd4146 /wd4189 /wd4201 /wd4324 /wd4351 /wd4127 /wd4505 /wd4530 /wd4702 /D_CRT_SECURE_NO_WARNINGS",
 	},
@@ -38,7 +38,7 @@ local configs = {
 		toolchain = "gcc",
 		cxx = "g++",
 
-		cxxflags = "-I . -c -x c++ -std=c++11 -msse2 -ffast-math -fno-exceptions -fno-rtti -fno-strict-aliasing -fno-strict-overflow -fdiagnostics-color",
+		cxxflags = "-I src -c -x c++ -std=c++11 -msse2 -ffast-math -fno-exceptions -fno-rtti -fno-strict-aliasing -fno-strict-overflow -fdiagnostics-color",
 		ldflags = "-lm -lpthread -ldl",
 		warnings = "-Wall -Wextra -Wno-unused-parameter -Wno-unused-function -Wshadow -Wcast-align -Wstrict-overflow -Wvla", -- -Wconversion
 	},
diff --git a/src/common.h b/src/common.h
@@ -4,13 +4,28 @@
 #include <stdint.h>
 #include <stddef.h>
 #include <stdlib.h>
+#include <string.h>
 #include <assert.h>
 
 #include "config.h"
 
-#define STRL( x ) ( x ), sizeof( x ) - 1
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+
+#define FATAL( form, ... ) \
+	do { \
+		printf( "[FATAL] " form, ##__VA_ARGS__ ); \
+		abort(); \
+	} while( 0 )
 
 #define STATIC_ASSERT( p ) static_assert( p, #p )
+#define ASSERT assert
+#define NONCOPYABLE( T ) T( const T & ) = delete; void operator=( const T & ) = delete;
+
+template< typename T, size_t N >
+char ( &ArrayCountObj( const T ( & )[ N ] ) )[ N ];
+#define ARRAY_COUNT( arr ) ( sizeof( ArrayCountObj( arr ) ) )
 
 template< typename T >
 constexpr T min( T a, T b ) {
@@ -35,3 +50,15 @@ inline To checked_cast( const From & from ) {
 	assert( From( result ) == from );
 	return result;
 }
+
+template< typename T >
+inline T * malloc_array( size_t count ) {
+	ASSERT( SIZE_MAX / count >= sizeof( T ) );
+	return ( T * ) malloc( count * sizeof( T ) );
+}
+
+template< typename T >
+inline T * realloc_array( T * old, size_t count ) {
+	ASSERT( SIZE_MAX / count >= sizeof( T ) );
+	return ( T * ) realloc( old, count * sizeof( T ) );
+}
diff --git a/src/lua/chat.lua b/src/lua/chat.lua
@@ -1,5 +1,3 @@
-local loop = ev.Loop.default
-
 local handleChat
 
 local CommandBytes = {
@@ -61,11 +59,9 @@ end
 
 local Client = { }
 
-function Client:new( socket, address, port )
-	socket:setoption( "keepalive", true )
-
+function Client:new( sock, address, port )
 	local client = {
-		socket = socket,
+		socket = sock,
 
 		address = address,
 		port = port,
@@ -80,7 +76,7 @@ function Client:new( socket, address, port )
 
 	table.insert( Clients, client )
 
-	socket:send( "CHAT:Hirve\n127.0.0.14050 " )
+	socket.send( sock, "CHAT:Hirve\n127.0.0.14050 " )
 
 	return client
 end
@@ -93,7 +89,7 @@ function Client:kill()
 	mud.print( "\n#s> Disconnected from %s!", self.name )
 
 	self.state = "killed"
-	self.socket:shutdown()
+	socket.close( self.socket )
 
 	for i, client in ipairs( Clients ) do
 		if client == self then
@@ -123,7 +119,7 @@ function mud.chatns( form, ... )
 
 	for _, client in ipairs( Clients ) do
 		if client.state == "connected" then
-			client.socket:send( data )
+			socket.send( client.socket, data )
 		end
 	end
 
@@ -138,31 +134,22 @@ end
 local function call( address, port )
 	mud.print( "\n#s> Calling %s:%d...", address, port )
 
-	local sock = socket.tcp()
-	sock:settimeout( 0 )
-
-	sock:connect( address, port )
-
-	ev.IO.new( function( loop, watcher )
-		local _, err = sock:receive( "*a" )
-
-		if err ~= "connection refused" then
-			local client = Client:new( sock, address, port )
-
-			ev.IO.new( function( loop, watcher )
-				dataHandler( client, loop, watcher )
-			end, sock:getfd(), ev.READ ):start( loop )
-
-			ev.IO.new( function( loop, watcher )
-				client.socket:send( CommandBytes.version .. "MudGangster" .. "\255" )
-				watcher:stop( loop )
-			end, sock:getfd(), ev.WRITE ):start( loop )
+	local client
+	local sock, err = socket.connect( address, port, function( sock, data )
+		if data then
+			assert( coroutine.resume( client.handler, data ) )
 		else
-			mud.print( "\n#s> Failed to call %s:%d", address, port )
+			client:kill()
 		end
+	end )
 
-		watcher:stop( loop )
-	end, sock:getfd(), ev.WRITE ):start( loop )
+	if not sock then
+		mud.print( "\n#s> Connection failed: %s", err )
+		return
+	end
+
+	client = Client:new( sock, address, port )
+	socket.send( client.socket, CommandBytes.version .. "MudGangster" .. "\255" )
 end
 
 mud.alias( "/call", {
@@ -187,7 +174,7 @@ local function sendPM( client, message )
 	local named = "\nHirve chats to you, '" .. message .. "'"
 	local data = CommandBytes.pm .. named .. "\n\255"
 
-	client.socket:send( data )
+	socket.send( client.socket, data )
 end
 
 mud.alias( "/silentpm", {
diff --git a/src/lua/connect.lua b/src/lua/connect.lua
@@ -1,113 +0,0 @@
-local DataHandler
-
-local LastAddress
-local LastPort
-
-local loop = ev.Loop.default
-
-function mud.disconnect()
-	if not mud.connected then
-		mud.print( "\n#s> You're not connected..." )
-
-		return
-	end
-
-	mud.connected = false
-
-	mud.kill()
-end
-
-function mud.connect( address, port )
-	if mud.connected then
-		mud.print( "\n#s> Already connected! (%s:%d)", LastAddress, LastPort )
-
-		return
-	end
-
-	if not address then
-		if not LastAddress then
-			mud.print( "\n#s> I need an address..." )
-			
-			return
-		end
-
-		address = LastAddress
-		port = LastPort
-	end
-
-	local sock = socket.tcp()
-	sock:settimeout( 0 )
-	sock:connect( address, port )
-
-	mud.print( "\n#s> Connecting to %s:%d...", address, port )
-
-	mud.connected = true
-	mud.lastInput = mud.now()
-
-	mud.send = function( data )
-		mud.lastInput = mud.now()
-
-		sock:send( data )
-	end
-
-	mud.kill = function()
-		sock:shutdown()
-	end
-
-	ev.IO.new( function( loop, watcher )
-		local _, err = sock:receive( "*a" )
-
-		if err ~= "connection refused" then
-			LastAddress = address
-			LastPort = port
-
-			ev.IO.new( function( loop, watcher )
-				local _, err, data = sock:receive( "*a" )
-
-				if not data then
-					data = _
-				end
-				
-				if data then
-					DataHandler( data )
-					mud.handleXEvents()
-				end
-				
-				if err == "closed" then
-					mud.print( "\n#s> Disconnected!" )
-
-					watcher:stop( loop )
-					sock:shutdown()
-
-					mud.connected = false
-
-					return
-				end
-			end, sock:getfd(), ev.READ ):start( loop )
-		else
-			mud.print( "\n#s> Refused!" )
-
-			mud.connected = false
-		end
-
-		watcher:stop( loop )
-	end, sock:getfd(), ev.WRITE ):start( loop )
-end
-
-mud.alias( "/con", {
-	[ "^$" ] = function()
-		mud.connect()
-	end,
-
-	[ "^(%S+)%s+(%d+)$" ] = function( address, port )
-		mud.connect( address, port )
-	end,
-}, "<ip> <port>" )
-
-mud.alias( "/dc", mud.disconnect )
-
-return {
-	init = function( dataHandler )
-		DataHandler = dataHandler
-	end,
-}
diff --git a/src/lua/handlers.lua b/src/lua/handlers.lua
@@ -254,6 +254,7 @@ local function handleCommand( input, hide )
 			end
 		end
 
+		print( mud.send )
 		mud.send( input .. "\n" )
 		mud.drawMain()
 	end 
@@ -284,8 +285,6 @@ local function handleMacro( key, shift, ctrl, alt )
 end
 
 local function handleClose()
-	ev.Loop.default:unloop()
-
 	mud.event( "shutdown" )
 end
 
@@ -295,5 +294,6 @@ return {
 	input = handleInput,
 	macro = handleMacro,
 	interval = interval.doIntervals,
+	socket = handleSocketData,
 	close = handleClose,
 }
diff --git a/src/lua/interval.lua b/src/lua/interval.lua
@@ -1,7 +1,7 @@
 local Intervals = { }
 
-local function doIntervals( loop, watcher )
-	local now = loop:update_now()
+local function doIntervals()
+	local now = mud.now()
 
 	for i = 1, #Intervals do
 		local event = Intervals[ i ]
@@ -12,10 +12,6 @@ local function doIntervals( loop, watcher )
 	end
 end
 
-function mud.now()
-	return ev.Loop.default:update_now()
-end
-
 function mud.interval( callback, interval, disabled )
 	enforce( callback, "callback", "function" )
 	enforce( interval, "interval", "number" )
diff --git a/src/lua/main.lua b/src/lua/main.lua
@@ -2,44 +2,32 @@ mud = {
 	connected = false,
 }
 
--- local function luarocks_path( opt, orig )
--- 	local pipe = io.popen( "luarocks path " .. opt, "r" )
--- 	if not pipe then
--- 		return nil
--- 	end
---
--- 	local contents = pipe:read( "*all" )
--- 	pipe:close()
---
--- 	if not contents then
--- 		return orig
--- 	end
---
--- 	return contents:gsub( "%s*$", "" ) .. ";" .. orig
--- end
---
--- package.path = luarocks_path( "--lr-path", package.path )
--- package.cpath = luarocks_path( "--lr-cpath", package.cpath )
-
 table.unpack = table.unpack or unpack
 
 require( "utils" )
 
-socket = require( "socket" )
-ev = require( "ev" )
-
 require( "event" )
 
 local script = require( "script" )
 local handlers = require( "handlers" )
 
-require( "chat" ).init( handlers.chat )
-require( "connect" ).init( handlers.data )
-
-local xFD, handleXEvents,
+local handleXEvents,
 	printMain, newlineMain, drawMain,
 	printChat, newlineChat, drawChat,
-	setHandlers, urgent, setStatus = ...
+	setHandlers, urgent, setStatus,
+	sock_connect, sock_send, sock_close,
+	get_time = ...
+
+local socket_api = {
+	connect = sock_connect,
+	send = sock_send,
+	close = sock_close,
+}
+
+local socket_data_handler = require( "socket" ).init( socket_api )
+
+require( "mud" ).init( handlers.data )
+require( "chat" ).init( handlers.chat )
 
 mud.printMain = printMain
 mud.newlineMain = newlineMain
@@ -50,20 +38,12 @@ mud.newlineChat = newlineChat
 mud.drawChat = drawChat
 
 mud.urgent = urgent
+mud.now = get_time
 
 require( "status" ).init( setStatus )
 
-setHandlers( handlers.input, handlers.macro, handlers.close )
-
-local loop = ev.Loop.default
+setHandlers( handlers.input, handlers.macro, handlers.close, socket_data_handler, handlers.interval )
 
 mud.handleXEvents = handleXEvents
 
-ev.IO.new( handleXEvents, xFD, ev.READ ):start( loop )
-ev.Timer.new( handlers.interval, 0.5, 0.5 ):start( loop )
-
 script.load()
-
-loop:loop()
-
-script.close()
diff --git a/src/lua/mud.lua b/src/lua/mud.lua
@@ -0,0 +1,81 @@
+local DataHandler
+local mud_socket
+
+local LastAddress
+local LastPort
+
+function mud.send( data )
+	mud.lastInput = mud.now()
+	socket.send( mud_socket, data )
+end
+
+function mud.disconnect()
+	mud.print( "\n#s> Disconnected!" )
+	mud.connected = false
+	socket.close( mud_socket )
+	mud_socket = nil
+end
+
+function mud.connect( address, port )
+	if mud.connected then
+		mud.print( "\n#s> Already connected! (%s:%d)", LastAddress, LastPort )
+
+		return
+	end
+
+	if not address then
+		if not LastAddress then
+			mud.print( "\n#s> I need an address..." )
+			
+			return
+		end
+
+		address = LastAddress
+		port = LastPort
+	end
+
+	mud.print( "\n#s> Connecting to %s:%d...", address, port )
+
+	local sock, err = socket.connect( address, port, function( sock, data )
+		if data then
+			DataHandler( data )
+		else
+			mud.kill()
+		end
+	end )
+
+	if not sock then
+		mud.print( "\n#s> Connection failed: %s", err )
+		return
+	end
+
+	LastAddress = address
+	LastPort = port
+
+	mud.connected = true
+	mud.lastInput = mud.now()
+end
+
+mud.alias( "/con", {
+	[ "^$" ] = function()
+		mud.connect()
+	end,
+
+	[ "^(%S+)%s+(%d+)$" ] = function( address, port )
+		mud.connect( address, port )
+	end,
+}, "<ip> <port>" )
+
+mud.alias( "/dc", function()
+	if mud.connected then
+		mud.disconnect()
+	else
+		mud.print( "\n#s> You're not connected..." )
+	end
+end )
+
+return {
+	init = function( dataHandler )
+		DataHandler = dataHandler
+	end,
+}
diff --git a/src/lua/socket.lua b/src/lua/socket.lua
@@ -0,0 +1,36 @@
+local socket_api
+local data_callbacks = { }
+
+local function connect( addr, port, cb )
+	local sock, err = socket_api.connect( addr, port )
+	if not sock then
+		return nil, err
+	end
+	data_callbacks[ sock ] = cb
+	return sock
+end
+
+local function close( sock )
+	socket_api.close( sock )
+	data_callbacks[ sock ] = nil
+end
+
+local function on_socket_data( sock, data )
+	-- data could be like { type = "data/failed/close/etc", data = ... }
+	-- need a failed to handle async connect
+	data_callbacks[ sock ]( sock, data )
+end
+
+return {
+	init = function( api )
+		socket_api = api
+
+		socket = {
+			connect = connect,
+			send = socket_api.send,
+			close = close,
+		}
+
+		return on_socket_data
+	end,
+}
diff --git a/src/main.cc b/src/main.cc
@@ -7,7 +7,7 @@ int main() {
 	input_init();
 	script_init();
 
-	// main loop is done in lua
+	event_loop();
 
 	script_term();
 	input_term();
diff --git a/src/platform_network.cc b/src/platform_network.cc
@@ -0,0 +1,262 @@
+#include "common.h"
+
+#include "platform.h"
+#include "platform_network.h"
+
+struct sockaddr_storage;
+
+static NetAddress sockaddr_to_netaddress( const struct sockaddr_storage & ss );
+static struct sockaddr_storage netaddress_to_sockaddr( const NetAddress & addr );
+static void setsockoptone( OSSocket fd, int level, int opt );
+
+#if PLATFORM_WINDOWS
+#include "win32_network.cc"
+#elif PLATFORM_UNIX
+#include "unix_network.cc"
+#else
+#error new platform
+#endif
+
+bool operator==( const NetAddress & lhs, const NetAddress & rhs ) {
+	if( lhs.type != rhs.type ) return false;
+	if( lhs.port != rhs.port ) return false;
+	if( lhs.type == NET_IPV4 ) return memcmp( &lhs.ipv4, &rhs.ipv4, sizeof( lhs.ipv4 ) ) == 0;
+	return memcmp( &lhs.ipv6, &rhs.ipv6, sizeof( lhs.ipv6 ) ) == 0;
+}
+
+bool operator!=( const NetAddress & lhs, const NetAddress & rhs ) {
+	return !( lhs == rhs );
+}
+
+static socklen_t sockaddr_size( const struct sockaddr_storage & ss ) {
+	return ss.ss_family == AF_INET ? sizeof( struct sockaddr_in ) : sizeof( struct sockaddr_in6 );
+}
+
+static NetAddress sockaddr_to_netaddress( const struct sockaddr_storage & ss ) {
+	NetAddress addr;
+	if( ss.ss_family == AF_INET ) {
+		const struct sockaddr_in & sa4 = ( const struct sockaddr_in & ) ss;
+
+		addr.type = NET_IPV4;
+		addr.port = ntohs( sa4.sin_port );
+		memcpy( &addr.ipv4, &sa4.sin_addr.s_addr, sizeof( addr.ipv4 ) );
+	}
+	else {
+		const struct sockaddr_in6 & sa6 = ( const struct sockaddr_in6 & ) ss;
+
+		addr.type = NET_IPV6;
+		addr.port = ntohs( sa6.sin6_port );
+		memcpy( &addr.ipv6, &sa6.sin6_addr.s6_addr, sizeof( addr.ipv6 ) );
+	}
+	return addr;
+}
+
+static struct sockaddr_storage netaddress_to_sockaddr( const NetAddress & addr ) {
+	struct sockaddr_storage ss;
+	if( addr.type == NET_IPV4 ) {
+		struct sockaddr_in & sa4 = ( struct sockaddr_in & ) ss;
+
+		ss.ss_family = AF_INET;
+		sa4.sin_port = htons( addr.port );
+		memcpy( &sa4.sin_addr.s_addr, &addr.ipv4, sizeof( addr.ipv4 ) );
+	}
+	else {
+		struct sockaddr_in6 & sa6 = ( struct sockaddr_in6 & ) ss;
+
+		ss.ss_family = AF_INET6;
+		sa6.sin6_port = htons( addr.port );
+		memcpy( &sa6.sin6_addr.s6_addr, &addr.ipv6, sizeof( addr.ipv6 ) );
+	}
+	return ss;
+}
+
+static void setsockoptone( OSSocket fd, int level, int opt ) {
+	int one = 1;
+	int ok = setsockopt( fd, level, opt, ( char * ) &one, sizeof( one ) );
+	if( ok == -1 ) {
+		FATAL( "setsockopt" );
+	}
+}
+
+UDPSocket net_new_udp( NonblockingBool nonblocking, u16 port ) {
+	UDPSocket sock;
+
+	sock.ipv4 = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
+	if( sock.ipv4 == INVALID_SOCKET ) {
+		FATAL( "socket" );
+	}
+	sock.ipv6 = socket( AF_INET6, SOCK_DGRAM, IPPROTO_UDP );
+	if( sock.ipv6 == INVALID_SOCKET ) {
+		FATAL( "socket" );
+	}
+
+	if( nonblocking == NET_NONBLOCKING ) {
+		make_socket_nonblocking( sock.ipv4 );
+		make_socket_nonblocking( sock.ipv6 );
+	}
+
+	if( port != 0 ) {
+		setsockoptone( sock.ipv4, SOL_SOCKET, SO_REUSEADDR );
+		setsockoptone( sock.ipv6, SOL_SOCKET, SO_REUSEADDR );
+		setsockoptone( sock.ipv6, IPPROTO_IPV6, IPV6_V6ONLY );
+	}
+
+	platform_init_sock( sock.ipv4 );
+	platform_init_sock( sock.ipv6 );
+
+	{
+		sockaddr_in my_addr4;
+		my_addr4.sin_family = AF_INET;
+		my_addr4.sin_port = htons( port );
+		my_addr4.sin_addr.s_addr = htonl( INADDR_ANY );
+		int ok = bind( sock.ipv4, ( struct sockaddr * ) &my_addr4, sizeof( my_addr4 ) );
+		if( ok == SOCKET_ERROR ) {
+			FATAL( "bind" );
+		}
+	}
+
+	{
+		sockaddr_in6 my_addr6;
+		my_addr6.sin6_family = AF_INET6;
+		my_addr6.sin6_port = htons( port );
+		my_addr6.sin6_addr = in6addr_any;
+		int ok = bind( sock.ipv6, ( struct sockaddr * ) &my_addr6, sizeof( my_addr6 ) );
+		if( ok == SOCKET_ERROR ) {
+			FATAL( "bind" );
+		}
+	}
+
+	return sock;
+}
+
+void net_send( UDPSocket sock, const void * data, size_t len, const NetAddress & addr ) {
+	struct sockaddr_storage ss = netaddress_to_sockaddr( addr );
+	socklen_t ss_size = sockaddr_size( ss );
+	OSSocket fd = addr.type == NET_IPV4 ? sock.ipv4 : sock.ipv6;
+	ssize_t ok = sendto( fd, ( const char * ) data, checked_cast< int >( len ), NET_SEND_FLAGS, ( struct sockaddr * ) &ss, ss_size );
+	if( ok == SOCKET_ERROR ) {
+		FATAL( "sendto" );
+	}
+}
+
+void net_destroy( UDPSocket * sock ) {
+	int ok4 = closesocket( sock->ipv4 );
+	if( ok4 == -1 ) {
+		FATAL( "closesocket" );
+	}
+	int ok6 = closesocket( sock->ipv6 );
+	if( ok6 == -1 ) {
+		FATAL( "closesocket" );
+	}
+	sock->ipv4 = INVALID_SOCKET;
+	sock->ipv6 = INVALID_SOCKET;
+}
+
+bool net_new_tcp( TCPSocket * sock, const NetAddress & addr, NonblockingBool nonblocking ) {
+	struct sockaddr_storage ss = netaddress_to_sockaddr( addr );
+	socklen_t ss_size = sockaddr_size( ss );
+
+	sock->fd = socket( ss.ss_family, SOCK_STREAM, IPPROTO_TCP );
+	if( sock->fd == INVALID_SOCKET ) {
+		FATAL( "socket" );
+	}
+
+	int ok = connect( sock->fd, ( const sockaddr * ) &ss, ss_size );
+	if( ok == -1 ) {
+		int ok_close = closesocket( sock->fd );
+		if( ok_close == -1 ) {
+			FATAL( "closesocket" );
+		}
+		// TODO: check for actual coding errors too
+		return false;
+	}
+
+	if( nonblocking == NET_NONBLOCKING ) {
+		make_socket_nonblocking( sock->fd );
+	}
+
+	setsockoptone( sock->fd, SOL_SOCKET, SO_KEEPALIVE );
+
+	platform_init_sock( sock->fd );
+
+	return true;
+}
+
+bool net_send( TCPSocket sock, const void * data, size_t len ) {
+	ssize_t sent = send( sock.fd, ( const char * ) data, len, NET_SEND_FLAGS );
+	if( sent < 0 ) return false;
+	return checked_cast< size_t >( sent ) == len;
+}
+
+TCPRecvResult net_recv( TCPSocket sock, void * buf, size_t buf_size, size_t * bytes_read, u32 timeout_ms ) {
+	while( true ) {
+		if( timeout_ms > 0 ) {
+			fd_set fds;
+			FD_ZERO( &fds );
+			FD_SET( sock.fd, &fds );
+
+			struct timeval tv;
+			tv.tv_sec = timeout_ms / 1000;
+			tv.tv_usec = ( timeout_ms % 1000 ) * 1000;
+
+			int ok = select( sock.fd + 1, &fds, NULL, NULL, &tv );
+			// TODO: update timeout
+			if( ok == 0 ) {
+				return TCP_TIMEOUT;
+			}
+			if( ok == -1 ) {
+				if( errno != EINTR ) continue;
+				FATAL( "select" );
+			}
+		}
+
+		ssize_t r = recv( sock.fd, ( char * ) buf, buf_size, 0 );
+		// TODO: this is not right on windows
+		if( r == -1 ) {
+			if( errno == EINTR ) continue;
+			if( errno == ECONNRESET ) return TCP_ERROR;
+			FATAL( "recv" );
+		}
+
+		*bytes_read = checked_cast< size_t >( r );
+		return r == 0 ? TCP_CLOSED : TCP_OK;
+	}
+}
+
+void net_destroy( TCPSocket * sock ) {
+	int ok = closesocket( sock->fd );
+	if( ok == -1 ) {
+		FATAL( "closesocket" );
+	}
+	sock->fd = INVALID_SOCKET;
+}
+
+size_t dns( const char * host, NetAddress * out, size_t n ) {
+	struct addrinfo hints;
+	memset( &hints, 0, sizeof( struct addrinfo ) );
+	hints.ai_family = AF_UNSPEC;
+	// TODO: figure out why ivp6 doesn't work on windows
+	hints.ai_family = AF_INET;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_protocol = IPPROTO_TCP;
+
+	struct addrinfo * addresses;
+	int ok = getaddrinfo( host, "http", &hints, &addresses );
+	if( ok != 0 )
+		return 0;
+
+	struct addrinfo * cursor = addresses;
+	size_t i = 0;
+	while( cursor != NULL && i < n ) {
+		out[ i ] = sockaddr_to_netaddress( *( struct sockaddr_storage * ) cursor->ai_addr );
+		cursor = cursor->ai_next;
+		i++;
+	}
+	freeaddrinfo( addresses );
+
+	return i;
+}
+
+bool dns_first( const char * host, NetAddress * address ) {
+	return dns( host, address, 1 ) == 1;
+}
diff --git a/src/platform_network.h b/src/platform_network.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "platform.h"
+
+#if PLATFORM_WINDOWS
+typedef s64 OSSocket;
+#elif PLATFORM_UNIX
+typedef int OSSocket;
+#else
+#error new platform
+#endif
+
+enum TransportProtocol { NET_UDP, NET_TCP };
+enum IPvX { NET_IPV4, NET_IPV6 };
+enum NonblockingBool { NET_BLOCKING, NET_NONBLOCKING };
+
+enum TCPRecvResult {
+	TCP_OK,
+	TCP_TIMEOUT,
+	TCP_CLOSED,
+	TCP_ERROR,
+};
+
+struct UDPSocket {
+	OSSocket ipv4, ipv6;
+};
+
+struct TCPSocket {
+	OSSocket fd;
+};
+
+struct IPv4 { u8 bytes[ 4 ]; };
+struct IPv6 { u8 bytes[ 16 ]; };
+
+struct NetAddress {
+        IPvX type;
+        union {
+                IPv4 ipv4;
+                IPv6 ipv6;
+        };
+        u16 port;
+};
+
+bool operator==( const NetAddress & lhs, const NetAddress & rhs );
+bool operator!=( const NetAddress & lhs, const NetAddress & rhs );
+
+void net_init();
+void net_term();
+
+UDPSocket net_new_udp( NonblockingBool nonblocking, u16 port = 0 );
+void net_send( UDPSocket sock, const void * data, size_t len, const NetAddress & addr );
+bool net_tryrecv( UDPSocket sock, void * buf, size_t len, NetAddress * addr, size_t * bytes_received );
+void net_destroy( UDPSocket * sock );
+
+bool net_new_tcp( TCPSocket * sock, const NetAddress & addr, NonblockingBool nonblocking );
+bool net_send( TCPSocket sock, const void * data, size_t len );
+TCPRecvResult net_recv( TCPSocket sock, void * buf, size_t buf_size, size_t * bytes_read, u32 timeout_ms = 0 );
+void net_destroy( TCPSocket * sock );
+
+bool dns_first( const char * host, NetAddress * address );
diff --git a/src/platform_time.h b/src/platform_time.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "platform.h"
+
+#if PLATFORM_WINDOWS
+#include "win32_time.h"
+#elif PLATFORM_OSX
+#include "darwin_time.h"
+#elif PLATFORM_UNIX
+#include "unix_time.h"
+#else
+#error new platform
+#endif
diff --git a/src/script.cc b/src/script.cc
@@ -2,6 +2,8 @@
 #include "platform.h"
 #include "ui.h"
 
+#include "platform_time.h"
+
 #include <lua.hpp>
 
 #if LUA_VERSION_NUM < 502
@@ -9,7 +11,7 @@
 #endif
 
 static const uint8_t lua_bytecode[] = {
-#include "build/lua_bytecode.h"
+#include "../build/lua_bytecode.h"
 };
 
 static lua_State * lua;
@@ -17,6 +19,17 @@ static lua_State * lua;
 static int inputHandlerIdx = LUA_NOREF;
 static int macroHandlerIdx = LUA_NOREF;
 static int closeHandlerIdx = LUA_NOREF;
+static int socketHandlerIdx = LUA_NOREF;
+static int intervalHandlerIdx = LUA_NOREF;
+
+static void pcall( int args, const char * err ) {
+	if( lua_pcall( lua, args, 0, 1 ) ) {
+		printf( "%s: %s\n", err, lua_tostring( lua, -1 ) );
+		exit( 1 );
+	}
+
+	assert( lua_gettop( lua ) == 1 );
+}
 
 void script_handleInput( const char * buffer, int len ) {
 	assert( inputHandlerIdx != LUA_NOREF );
@@ -24,10 +37,12 @@ void script_handleInput( const char * buffer, int len ) {
 	lua_rawgeti( lua, LUA_REGISTRYINDEX, inputHandlerIdx );
 	lua_pushlstring( lua, buffer, len );
 
-	lua_call( lua, 1, 0 );
+	pcall( 1, "script_handleInput" );
 }
 
 void script_doMacro( const char * key, int len, bool shift, bool ctrl, bool alt ) {
+	assert( macroHandlerIdx != LUA_NOREF );
+
 	lua_rawgeti( lua, LUA_REGISTRYINDEX, macroHandlerIdx );
 
 	lua_pushlstring( lua, key, len );
@@ -36,15 +51,35 @@ void script_doMacro( const char * key, int len, bool shift, bool ctrl, bool alt 
 	lua_pushboolean( lua, ctrl );
 	lua_pushboolean( lua, alt );
 
-	lua_call( lua, 4, 0 );
+	pcall( 4, "script_doMacro" );
 }
 
 void script_handleClose() {
 	assert( closeHandlerIdx != LUA_NOREF );
 
 	lua_rawgeti( lua, LUA_REGISTRYINDEX, closeHandlerIdx );
+	pcall( 0, "script_handleClose" );
+}
+
+void script_socketData( void * sock, const char * data, size_t len ) {
+	assert( socketHandlerIdx != LUA_NOREF );
+
+	lua_rawgeti( lua, LUA_REGISTRYINDEX, socketHandlerIdx );
 
-	lua_call( lua, 0, 0 );
+	lua_pushlightuserdata( lua, sock );
+	if( data == NULL )
+		lua_pushnil( lua );
+	else
+		lua_pushlstring( lua, data, len );
+
+	pcall( 2, "script_socketData" );
+}
+
+void script_fire_intervals() {
+	assert( intervalHandlerIdx != LUA_NOREF );
+
+	lua_rawgeti( lua, LUA_REGISTRYINDEX, intervalHandlerIdx );
+	pcall( 0, "script_fire_intervals" );
 }
 
 namespace {
@@ -66,6 +101,44 @@ static void generic_print( F * f, lua_State * L ) {
 	f( str, len, fg, bg, bold );
 }
 
+extern "C" int mud_connect( lua_State * L ) {
+	const char * host = luaL_checkstring( L, 1 );
+	int port = luaL_checkinteger( L, 2 );
+
+	const char * err;
+	void * sock = platform_connect( &err, host, port );
+	if( sock != NULL ) {
+		lua_pushlightuserdata( lua, sock );
+		return 1;
+	}
+
+	lua_pushnil( lua );
+	lua_pushstring( lua, err );
+
+	return 2;
+}
+
+extern "C" int mud_send( lua_State * L ) {
+	luaL_argcheck( L, lua_isuserdata( L, 1 ) == 1, 1, "expected socket" );
+	void * sock = lua_touserdata( L, 1 );
+
+	const char * data = luaL_checkstring( L, 2 );
+	size_t len = luaL_len( L, 2 );
+
+	platform_send( sock, data, len );
+
+	return 0;
+}
+
+extern "C" int mud_close( lua_State * L ) {
+	luaL_argcheck( L, lua_isuserdata( L, 1 ) == 1, 1, "expected socket" );
+	void * sock = lua_touserdata( L, 1 );
+
+	platform_close( sock );
+
+	return 0;
+}
+
 extern "C" int mud_printMain( lua_State * L ) {
 	generic_print( ui_main_print, L );
 	return 0;
@@ -136,7 +209,11 @@ extern "C" int mud_setHandlers( lua_State * L ) {
 	luaL_argcheck( L, lua_type( L, 1 ) == LUA_TFUNCTION, 1, "expected function" );
 	luaL_argcheck( L, lua_type( L, 2 ) == LUA_TFUNCTION, 2, "expected function" );
 	luaL_argcheck( L, lua_type( L, 3 ) == LUA_TFUNCTION, 3, "expected function" );
+	luaL_argcheck( L, lua_type( L, 4 ) == LUA_TFUNCTION, 4, "expected function" );
+	luaL_argcheck( L, lua_type( L, 5 ) == LUA_TFUNCTION, 5, "expected function" );
 
+	intervalHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX );
+	socketHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX );
 	closeHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX );
 	macroHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX );
 	inputHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX );
@@ -149,6 +226,11 @@ extern "C" int mud_urgent( lua_State * L ) {
 	return 0;
 }
 
+extern "C" int mud_now( lua_State * L ) {
+	lua_pushnumber( L, get_time() );
+	return 1;
+}
+
 } // anon namespace
 
 #if PLATFORM_WINDOWS
@@ -175,7 +257,6 @@ void script_init() {
 		exit( 1 );
 	}
 
-	lua_pushinteger( lua, ui_display_fd() );
 	lua_pushcfunction( lua, mud_handleXEvents );
 
 	lua_pushcfunction( lua, mud_printMain );
@@ -192,10 +273,13 @@ void script_init() {
 
 	lua_pushcfunction( lua, mud_setStatus );
 
-	if( lua_pcall( lua, 11, 0, -13 ) ) {
-		printf( "Error running main.lua: %s\n", lua_tostring( lua, -1 ) );
-		exit( 1 );
-	}
+	lua_pushcfunction( lua, mud_connect );
+	lua_pushcfunction( lua, mud_send );
+	lua_pushcfunction( lua, mud_close );
+
+	lua_pushcfunction( lua, mud_now );
+
+	pcall( 14, "Error running main.lua" );
 }
 
 void script_term() {
diff --git a/src/script.h b/src/script.h
@@ -1,9 +1,13 @@
 #pragma once
 
+#include <stddef.h>
+
 // TODO: should be size_t here?
 void script_handleInput( const char * buffer, int len );
 void script_doMacro( const char * key, int len, bool shift, bool ctrl, bool alt );
 void script_handleClose();
+void script_socketData( void * sock, const char * data, size_t len );
+void script_fire_intervals();
 
 void script_init();
 void script_term();
diff --git a/src/ui.h b/src/ui.h
@@ -39,13 +39,17 @@ void ui_chat_print( const char * str, size_t len, Colour fg, Colour bg, bool bol
 
 void ui_fill_rect( int left, int top, int width, int height, Colour colour, bool bold );
 void ui_draw_char( int left, int top, char c, Colour colour, bool bold, bool bold_font = false );
-void ui_dirty( int left, int top, int right, int bottom ); // TODO: x/y + w/h?
+void ui_dirty( int left, int top, int width, int height );
 
 void ui_get_font_size( int * fw, int * fh );
 
 void ui_urgent();
 
-int ui_display_fd(); // TODO: very x11 specific!
-
 void ui_init();
 void ui_term();
+
+void * platform_connect( const char ** err, const char * host, int port );
+void platform_send( void * sock, const char * data, size_t len );
+void platform_close( void * sock );
+
+void event_loop();
diff --git a/src/unix_network.cc b/src/unix_network.cc
@@ -0,0 +1,63 @@
+#include <arpa/inet.h>
+#include <sys/types.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <unistd.h>
+
+#define INVALID_SOCKET ( -1 )
+#define SOCKET_ERROR ( -1 )
+
+#define closesocket close
+
+#if PLATFORM_OSX
+static int NET_SEND_FLAGS = 0;
+#else
+static int NET_SEND_FLAGS = MSG_NOSIGNAL;
+#endif
+
+void net_init() { }
+void net_term() { }
+
+static void make_socket_nonblocking( int fd ) {
+	int flags = fcntl( fd, F_GETFL, 0 );
+	if( flags == -1 ) FATAL( "fcntl F_GETFL" );
+	int ok = fcntl( fd, F_SETFL, flags | O_NONBLOCK );
+	if( ok == -1 ) FATAL( "fcntl F_SETFL" );
+}
+
+static void platform_init_sock( int fd ) {
+#if PLATFORM_OSX
+	setsockoptone( fd, SOL_SOCKET, SO_NOSIGPIPE );
+#endif
+}
+
+bool net_tryrecv( UDPSocket sock, void * buf, size_t len, NetAddress * addr, size_t * bytes_received ) {
+	struct sockaddr_storage sa;
+
+	socklen_t sa_size = sizeof( struct sockaddr_in );
+	ssize_t received4 = recvfrom( sock.ipv4, buf, len, 0, ( struct sockaddr * ) &sa, &sa_size );
+	if( received4 != -1 ) {
+		*addr = sockaddr_to_netaddress( sa );
+		*bytes_received = size_t( received4 );
+		return true;
+	}
+	if( received4 == -1 && errno != EAGAIN ) {
+		FATAL( "recvfrom" );
+	}
+
+	sa_size = sizeof( struct sockaddr_in6 );
+	ssize_t received6 = recvfrom( sock.ipv6, buf, len, 0, ( struct sockaddr * ) &sa, &sa_size );
+	if( received6 != -1 ) {
+		*addr = sockaddr_to_netaddress( sa );
+		*bytes_received = size_t( received6 );
+		return true;
+	}
+	if( received6 == -1 && errno != EAGAIN ) {
+		FATAL( "recvfrom" );
+	}
+
+	return false;
+}
diff --git a/src/unix_time.h b/src/unix_time.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <time.h>
+
+inline double get_time() {
+	struct timespec ts;
+	clock_gettime( CLOCK_MONOTONIC, &ts );
+
+	return double( ts.tv_sec ) + ts.tv_nsec / 1e9;
+}
diff --git a/src/win32_network.cc b/src/win32_network.cc
@@ -0,0 +1,61 @@
+#include <winsock2.h>
+#include <ws2tcpip.h>
+
+static int NET_SEND_FLAGS = 0;
+
+void net_init() {
+	WSADATA wsa_data;
+	if( WSAStartup( MAKEWORD( 2, 2 ), &wsa_data ) == SOCKET_ERROR ) {
+		FATAL( "WSAStartup" );
+	}
+}
+
+void net_term() {
+	if( WSACleanup() == SOCKET_ERROR ) {
+		FATAL( "WSACleanup" );
+	}
+}
+
+static void make_socket_nonblocking( SOCKET fd ) {
+	u_long one = 1;
+	int ok = ioctlsocket( fd, FIONBIO, &one );
+	if( ok == SOCKET_ERROR ) {
+		FATAL( "ioctlsocket" );
+	}
+}
+
+static void platform_init_sock( SOCKET fd ) { }
+
+bool net_tryrecv( UDPSocket sock, void * buf, size_t len, NetAddress * addr, size_t * bytes_received ) {
+	struct sockaddr_storage sa;
+
+	socklen_t sa_size = sizeof( struct sockaddr_in );
+	ssize_t received4 = recvfrom( sock.ipv4, ( char * ) buf, len, 0, ( struct sockaddr * ) &sa, &sa_size );
+	if( received4 != SOCKET_ERROR ) {
+		*addr = sockaddr_to_netaddress( sa );
+		*bytes_received = size_t( received4 );
+		return true;
+	}
+	else {
+		int error = WSAGetLastError();
+		if( error != WSAEWOULDBLOCK && error != WSAECONNRESET ) {
+			FATAL( "recvfrom" );
+		}
+	}
+
+	sa_size = sizeof( struct sockaddr_in6 );
+	ssize_t received6 = recvfrom( sock.ipv6, ( char * ) buf, len, 0, ( struct sockaddr * ) &sa, &sa_size );
+	if( received6 != SOCKET_ERROR ) {
+		*addr = sockaddr_to_netaddress( sa );
+		*bytes_received = size_t( received6 );
+		return true;
+	}
+	else {
+		int error = WSAGetLastError();
+		if( error != WSAEWOULDBLOCK && error != WSAECONNRESET ) {
+			FATAL( "recvfrom" );
+		}
+	}
+
+	return false;
+}
diff --git a/src/win32_time.h b/src/win32_time.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <windows.h>
+
+inline double get_time() {
+	LARGE_INTEGER counter;
+	QueryPerformanceCounter( &counter );
+
+	LARGE_INTEGER freq;
+	QueryPerformanceFrequency( &freq );
+
+	return double( counter.QuadPart ) / double( freq.QuadPart );
+}
diff --git a/src/x11.cc b/src/x11.cc
@@ -1,4 +1,5 @@
 #include <err.h>
+#include <poll.h>
 
 #include <X11/Xutil.h>
 #include <X11/XKBlib.h>
@@ -10,6 +11,69 @@
 #include "script.h"
 #include "textbox.h"
 
+#include "platform_network.h"
+
+struct Socket {
+	TCPSocket sock;
+	bool in_use;
+};
+
+static Socket sockets[ 128 ];
+
+static bool closing = false;
+
+void * platform_connect( const char ** err, const char * host, int port ) {
+	size_t idx;
+	{
+		bool ok = false;
+		for( size_t i = 0; i < ARRAY_COUNT( sockets ); i++ ) {
+			if( !sockets[ i ].in_use ) {
+				idx = i;
+				ok = true;
+				break;
+			}
+		}
+
+		if( !ok ) {
+			*err = "too many connections";
+			return NULL;
+		}
+	}
+
+	NetAddress addr;
+	{
+		bool ok = dns_first( host, &addr );
+		if( !ok ) {
+			*err = "couldn't resolve hostname"; // TODO: error from dns_first
+			return NULL;
+		}
+	}
+	addr.port = checked_cast< u16 >( port );
+
+	TCPSocket sock;
+	bool ok = net_new_tcp( &sock, addr, NET_BLOCKING );
+	if( !ok ) {
+		*err = "net_new_tcp";
+		return NULL;
+	}
+
+	sockets[ idx ].sock = sock;
+	sockets[ idx ].in_use = true;
+
+	return &sockets[ idx ];
+}
+
+void platform_send( void * vsock, const char * data, size_t len ) {
+	Socket * sock = ( Socket * ) vsock;
+	net_send( sock->sock, data, len );
+}
+
+void platform_close( void * vsock ) {
+	Socket * sock = ( Socket * ) vsock;
+	net_destroy( &sock->sock );
+	sock->in_use = false;
+}
+
 struct {
 	Display * display;
 	int screen;
@@ -461,6 +525,7 @@ static void event_mouse_move( XEvent * xevent ) {
 static void event_message( XEvent * xevent ) {
 	if( ( Atom ) xevent->xclient.data.l[ 0 ] == wmDeleteWindow ) {
 		script_handleClose();
+		closing = true;
 	}
 }
 
@@ -736,6 +801,10 @@ static void initStyle() {
 }
 
 void ui_init() {
+	for( Socket & s : sockets ) {
+		s.in_use = false;
+	}
+
 	UI = { };
 
 	textbox_init( &UI.main_text, SCROLLBACK_SIZE );
@@ -831,3 +900,64 @@ void ui_term() {
 	XDestroyWindow( UI.display, UI.window );
 	XCloseDisplay( UI.display );
 }
+
+static Socket * socket_from_fd( int fd ) {
+	for( Socket & sock : sockets ) {
+		if( sock.in_use && sock.sock.fd == fd ) {
+			return &sock;
+		}
+	}
+
+	return NULL;
+}
+
+void event_loop() {
+	while( !closing ) {
+		pollfd fds[ ARRAY_COUNT( sockets ) + 1 ] = { };
+		nfds_t num_fds = 1;
+
+		fds[ 0 ].fd = ConnectionNumber( UI.display );
+		fds[ 0 ].events = POLLIN;
+
+		for( Socket sock : sockets ) {
+			if( sock.in_use ) {
+				fds[ num_fds ].fd = sock.sock.fd;
+				fds[ num_fds ].events = POLLIN;
+				num_fds++;
+			}
+		}
+
+		int ok = poll( fds, num_fds, 500 );
+		if( ok == -1 )
+			FATAL( "poll" );
+
+		if( ok == 0 ) {
+			script_fire_intervals();
+			continue;
+		}
+
+		for( size_t i = 1; i < ARRAY_COUNT( fds ); i++ ) {
+			if( fds[ i ].revents & POLLIN ) {
+				Socket * sock = socket_from_fd( fds[ i ].fd );
+				assert( sock != NULL );
+
+				char buf[ 8192 ];
+				size_t n;
+				TCPRecvResult res = net_recv( sock->sock, buf, sizeof( buf ), &n );
+				if( res == TCP_OK ) {
+					script_socketData( sock, buf, n );
+				}
+				else if( res == TCP_CLOSED ) {
+					script_socketData( sock, NULL, 0 );
+				}
+				else {
+					FATAL( "net_recv" );
+				}
+			}
+		}
+
+		if( fds[ 0 ].events & POLLIN ) {
+			ui_handleXEvents();
+		}
+	}
+}