HeavyThing - examples/simple_socket/simple_socket.asm

Jeff Marrison

	; ------------------------------------------------------------------------
	; HeavyThing x86_64 assembly language library and showcase programs
	; Copyright © 2015-2018 2 Ton Digital 
	; Homepage: https://2ton.com.au/
	; Author: Jeff Marrison <jeff@2ton.com.au>
	;       
	; This file is part of the HeavyThing library.
	;       
	; HeavyThing is free software: you can redistribute it and/or modify
	; it under the terms of the GNU General Public License, or
	; (at your option) any later version.
	;       
	; HeavyThing is distributed in the hope that it will be useful, 
	; but WITHOUT ANY WARRANTY; without even the implied warranty of
	; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
	; GNU General Public License for more details.
	;       
	; You should have received a copy of the GNU General Public License along
	; with the HeavyThing library. If not, see <http://www.gnu.org/licenses/>.
	; ------------------------------------------------------------------------
	; simple_socket.asm: A quick example of both host-based and IPv4 based
	; outbound socket communications (client-side).
	;
	; first things first, include the library defaults, and the
	; library main include:
include '../../ht_defaults.inc'
include '../../ht.inc'
	
	; all HeavyThing epoll-based goods are "virtual method table" based, which
	; means that in order to be useful, we need to "override" the library-
	; provided ones (the library provided ones do work, but do nothing by
	; design).
	; So, for this example, we copy the epoll$default_vtable (from epoll.inc)
	; and modify it to include our custom functions:
	; (note: the dalign macro aligns the table per the ht_defaults.inc setting)
dalign
example_vtable:
	; the original epoll$default_vtable looks like:
        ; dq      epoll$destroy, epoll$clone, io$connected, epoll$send, epoll$receive, io$error, io$timeout
	; our modified one:
        dq      epoll$destroy, epoll$clone, example_connected, epoll$send, example_receive, example_error, example_timeout


	; we will keep a global variable for our hostname argument and port number:
globals
{
	hostname	dq	0
	portnumber	dd	0
	; also, a boolean as to whether or not we successfully connected
	; (see notes below for DNS-based connection attempts)
	hasconnected	dd	0
}


	; and our function definitions:


	; this gets called [obviously] when/if the socket is actually connected and ready:
	; single argument gets passed in rdi: the epoll object
falign
example_connected:
	prolog	example_connected
	mov	dword [hasconnected], 1
	; hangon to our epoll object:
	push	rbx r12 r13
	mov	rbx, rdi
	; send a message to stderr indicating we are under way:
	mov	rdi, .connectmsg
	call	string$to_stderrln
	; so for our simple example, we'll do three string$concat ops
	; to form a very basic HTTP/1.0 request header:
	mov	rdi, .get1
	mov	rsi, [hostname]
	call	string$concat
	mov	r12, rax
	mov	rdi, rax
	mov	rsi, .get2
	call	string$concat
	mov	rdi, r12
	mov	r12, rax
	call	heap$free
	; so r12 now contains a string version of our request
	; but to send it out, we need UTF8:
	mov	rdi, r12
	call	string$utf8_length
	mov	rdi, rax
	call	heap$alloc
	mov	r13, rax
	mov	rdi, r12
	mov	rsi, rax
	call	string$to_utf8
	; that returns the # of bytes (same as utf8_length does)
	; so we can reuse that for our send operation:
	mov	rdi, rbx		; epoll object
	mov	rsi, r13		; UTF8 request text
	mov	edx, eax		; bytes to send
	call	epoll$send
	; cleanup after ourselves:
	mov	rdi, r13
	call	heap$free
	mov	rdi, r12
	call	heap$free
	pop	r13 r12 rbx
	epilog
cleartext .connectmsg, '[stderr]: Connected, sending HTTP/1.0 GET request.'
cleartext .get1, 'GET / HTTP/1.0',13,10,'Host: '
cleartext .get2, 13,10,13,10


	; this [obviously] gets called when data arrives
	; three arguments: rdi == epoll object, rsi == ptr to data, rdx == lenght of same
	; if we return 0 in eax, the epoll/socket stays alive, 1 in eax == destroys the epoll object
falign
example_receive:
	prolog	example_receive

	; some notes here: the epoll.inc functionality always "accumulates" data it receives
	; in a buffer object, and per the commentary that accompanies epoll$receive, we must
	; "drain" it (though as you might have guessed, there are cases when accumulating
	; by default is a nice feature)

	; all we want to do is send what we receive straight to stdout, and we don't need
	; the HeavyThing to do that:
	mov	eax, syscall_write
	push	qword [rdi+epoll_inbuf_ofs]
	mov	edi, 1		; fd == stdout
	; rsi already valid, so is rdx
	syscall
	pop	rdi
	; we don't want the epoll layer to accumulate, so reset it:
	call	buffer$reset
	; per the above commentary, we don't want the epoll layer to destroy us, return 0
	xor	eax, eax
	epilog




	; single argument gets passed in rdi: the epoll object
	; this is a notification only, the epoll object will get destroyed soon after this call
	; (this can happen both for actual errors and socket closes, we aren't interested in the
	; difference thanks to most actual errors being raised long before this happens)
falign
example_error:
	prolog	example_error

	; so for a non-DNS based lookup, this is our read error (or connect fail, etc)
	; for DNS based, this could mean that the lookup itself failed also (which is why we kept
	; track of hasconnected in the example_connected) function.

	cmp	dword [hasconnected], 0
	je	.baddns_or_connrefused

	; otherwise, just output something to stderr and be done:
	mov	rdi, .closed
	call	string$to_stderrln
	mov	eax, syscall_exit
	xor	edi, edi
	syscall
	epilog
cleartext .closed, '[stderr]: Connection closed'
cleartext .dns_or_refused, '[stderr]: DNS lookup failed or connection refused.'
calign
.baddns_or_connrefused:
	mov	rdi, .dns_or_refused
	call	string$to_stderrln
	mov	eax, syscall_exit
	mov	edi, 1		; exit status 1 for error
	syscall
	epilog


	; single argument in rdi: the epoll object
	; if we return false, object will stay alive, return 1 in eax == epoll object will get destroyed
falign
example_timeout:
	prolog	example_timeout
	; just bailout with a stderr message:
	mov	rdi, .timeoutmsg
	call	string$to_stderrln
	mov	eax, syscall_exit
	mov	edi, 1		; exit status 1 for error
	syscall
	epilog
cleartext .timeoutmsg, '[stderr]: Connection timed out.'


	
	; program entry point:
public _start
falign
_start:
	; every HeavyThing program needs to start with a call to initialise it
	call	ht$init

	; we expect to receive at minimum a hostname or IPv4 argument, possibly a port number
	cmp	dword [argc], 2
	jb	.usage
	cmp	dword [argc], 3
	ja	.usage
	; since [argv] is a list of strings, pop argv[0] off the front and get rid of it (our program name)
	mov	rdi, [argv]
	call	list$pop_front
	mov	rdi, rax
	call	heap$free
	; next arg is either a hostname, or an IPv4
	mov	rdi, [argv]
	call	list$pop_front
	; store that into our global defined above:
	mov	[hostname], rax
	mov	dword [portnumber], 80
	cmp	dword [argc], 3
	jne	.noportargument
	mov	rdi, [argv]
	call	list$pop_front	; pop_back would work of course here too
	; hang on to the string
	push	rax
	; convert that to an integer and make sure it is okay
	mov	rdi, rax
	call	string$to_int
	cmp	rax, 0
	jle	.badport
	cmp	rax, 65536
	jge	.badport
	mov	dword [portnumber], eax
	pop	rdi
	call	heap$free
.noportargument:
	; try and convert the hostname and port to an inet_addr...
	; we need an inet_addr worth of stackspace:
	sub	rsp, sockaddr_in_size		; 16 bytes for IPv4
						; note here for unix addresses (110 bytes), we'd want to
						; maintain stack alignment
	mov	rdi, rsp
	mov	rsi, [hostname]
	mov	edx, dword [portnumber]
	call	inet_addr
	test	eax, eax
	jz	.dns_based
	; inet_addr returned 1 for success, so now we can proceed with epoll goods

	mov	rdi, example_vtable		; our modified vtable from above
	xor	esi, esi			; no extra space required in the epoll object
	call	epoll$new
	; we need to set a timeout for connect, say 30 seconds for our example:
	mov	ecx, 30000
	; timeouts get specified in milliseconds, and we could call epoll$set_readtimeout
	; but it is a one-liner function so we'll just do it here directly:
	mov	[rax+epoll_readtimeout_ofs], rcx
	; epoll outbound needs rdi==address, esi==length of same, rdx == epoll object
	mov	rdi, rsp
	mov	esi, sockaddr_in_size		; 16 bytes for IPv4
	mov	rdx, rax
	call	epoll$outbound
	; so if that fails, connect() failed, so bailout here and now:
	test	eax, eax
	jz	.connectfail
	; otherwise, pass control to epoll$run (which never returns)
	call	epoll$run
	; not reached.
calign
.dns_based:
	; so if we get to here, inet_addr said our hostname argument isn't an IPv4
	; ... so we will use DNS to do our outbound.
	; first: undo our previous stack modification since we aren't dealing directly
	; with IPv4 sockaddr_in
	add	rsp, sockaddr_in_size
	;
	; some notes here: our example_error function will get called in the event of
	; a DNS lookup failure, and that is why we kept an additional global in this
	; example for whether or not we connected or not...
	; in a real world use case, we'd have either created an io.inc chain for our
	; epoll object, or requested additional space during epoll$new and used the
	; extra space to store our own custom variables that get carried around with
	; each connection/socket object... our simple example is a one-shot though
	; so we didn't bother with any of that.
	;
	; create an epoll object with our custom vtable:
	mov	rdi, example_vtable		; our modified vtable from above
	xor	esi, esi			; no extra space required in the epoll object
	call	epoll$new
	; we need to set a timeout for connect, say 30 seconds for our example:
	mov	ecx, 30000
	; timeouts get specified in milliseconds, and we could call epoll$set_readtimeout
	; but it is a one-liner function so we'll just do it here directly:
	mov	[rax+epoll_readtimeout_ofs], rcx
	; epoll$outbound_hostname needs string hostname, port, and epoll object:
	mov	rdi, [hostname]
	mov	esi, dword [portnumber]
	mov	rdx, rax
	call	epoll$outbound_hostname

	; pass control to epoll$run (which never returns)
	call	epoll$run
	; not reached.
cleartext .badportstr, 'Port specified is not between 1..65535.'
cleartext .connectfailstr, 'connect() failed.'
calign
.badport:
	mov	rdi, .badportstr
	jmp	.error_exit
.connectfail:
	mov	rdi, .connectfailstr
	jmp	.error_exit
calign
.usage:
	mov	rdi, .usagestr
.error_exit:
	call	string$to_stderrln
	mov	eax, syscall_exit
	mov	edi, 1
	syscall
cleartext .usagestr, 'usage: ./simple_socket hostname_or_IPv4 [port]',10,'  If port is not specified, defaults to 80.'

	; include the global data segment:
include '../../ht_data.inc'