~aleteoryx/nex.nelua

2716ae08bc3884cc8a26b95b6f858b1dcddb7c99 — Aleteoryx 17 days ago
initial commit, with a lot done already
3 files changed, 212 insertions(+), 0 deletions(-)

A LICENSE
A README.md
A socket.nelua
A  => LICENSE +18 -0
@@ 1,18 @@
-- TPL Public Domain Dedication v1.0
-- <https://amehut.dev/~aleteoryx/tpl>
------------------------------------------------------------------------

This repository is dedicated entirely to the public domain. The
creator(s) waive all intellectual property rights to the work as much
as is possible in any relevant jurisdictions.

In other words, do what you want.

THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

A  => README.md +4 -0
@@ 1,4 @@
# nex.nelua

nightfall express and nightfall postal service servers written in the
[nelua](https://nelua.io) programming language

A  => socket.nelua +190 -0
@@ 1,190 @@
--[[
  an asynchronous socket library.
  if i feel like making a full executor work this might get more complicated,
  but for now it's a standalone function.
]]

-- c imports
## cinclude '<sys/socket.h>'
local AF_INET: cint <cimport, nodecl>
local AF_INET6: cint <cimport, nodecl>
-- local AF_UNIX: cint <cimport, nodecl>

local SOCK_STREAM: cint <cimport, nodecl>
-- local SOCK_DGRAM: cint <cimport, nodecl>
-- local SOCK_RAW: cint <cimport, nodecl>

local MSG_DONTWAIT: cint <cimport, nodecl>

local function c_socket(domain: cint, type: cint, protocol: cint): cint <cimport 'socket', nodecl> end
local function c_accept(fd: cint, address: pointer, address_len: *cint): cint <cimport 'accept', nodecl> end
local function c_listen(fd: cint, backlog: cint): cint <cimport 'listen', nodecl> end

local function c_send(fd: cint, buf: *[0]byte, len: usize, flags: cint): isize <cimport 'send', nodecl> end

## cinclude '<arpa/inet.h>'
local function c_inet_pton(af: cint, src: cstring <const>, dst: pointer): cint <cimport 'inet_pton', nodecl> end
local function c_htons(hostshort: uint16): uint16 <cimport 'htons'> end
local function c_ntohs(hostshort: uint16): uint16 <cimport 'ntohs'> end


## cinclude '<sys/epoll.h>'
local EPOLL_CLOEXEC: cint <cimport, nodecl>

local EPOLL_CTL_ADD: cint <cimport, nodecl>
local EPOLL_CTL_MOD: cint <cimport, nodecl>
local EPOLL_CTL_DEL: cint <cimport, nodecl>

## for i,epoll_val in ipairs({ 'IN', 'OUT', 'RDHUP', 'PRI', 'ERR', 'HUP', 'ET'}) do
local #|'EPOLL' .. epoll_val|# : uint32 <cimport, nodecl>
## end

local epoll_data = @union{
  ptr: pointer,
  fd: cint,
  u32: uint32,
  u64: uint64
}
local epoll_event = @record{
  events: uint32,
  data: epoll_data
}

local function c_epoll_create(flags: cint): cint <cimport 'epoll_create1', nodecl> end
local function c_epoll_ctl(epfd: cint, op: cint, fd: cint, event: *epoll_event): cint <cimport 'epoll_ctl', nodecl> end
local function c_epoll_wait(epfd: cint, events: *[0]epoll_event, maxevents: cint, timeout: cint): cint <cimport 'epoll_wait', nodecl> end


## cinclude '<unistd.h>'
local function c_close(fd: cint): cint <cimport 'close', nodecl> end


## cinclude '<netinet/in.h>'


-- end c imports

require 'string'
require 'coroutine'
require 'math'
require 'C.stdio'
require 'allocators.pool'


local function fakeuse(...: varargs) end

-- 192.168.2.1:2000 -> 192.168.2.1,2000
local inet_re = '^(%d?%d?%d%.%d?%d?%d%.%d?%d?%d%.%d?%d?%d):(%d?%d?%d?%d?%d)$'
-- {::1}:2000 -> ::1,2000
local inet6_re = '^{([^}]+)}:(%d?%d?%d?%d?%d)$'

local function die_errno(cond: boolean, msg: string): void
  if cond then
    C.perror(nilptr)
    error(msg)
  end
end

local handler_req = @enum{
  init = 0,
  read_line,
  write,
  close
}

local handler_state = @record{
  fd: cint,
  co: coroutine,
  last_req: handler_req
}

function handler_state:step(epfd: cint, state: *handler_state): (boolean)
  return false
end

local function listen_sock(sock: cint, handler: function(): void): void
  die_errno(c_listen(sock, 32) ~= 0, "couldn't listen on TCP socket")
  local epfd = c_epoll_create(0)
  die_errno(epfd == -1, "couldn't create epoll instance")

  -- opt out of GC because we give references to the kernel
  local alloc: PoolAllocator(@handler_state, 1024) -- probably overkill on max size

  local sock_event: epoll_event = {
    events = EPOLLIN,
    data = { ptr = nilptr }
  }

  c_epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &sock_event)

  while true do
    local maxevents <comptime> = 8
    local events: [maxevents]epoll_event = {}
    local event_count = c_epoll_wait(epfd, &events, maxevents, -1)
    die_errno(event_count == -1, "couldn't wait on epoll instance")
    print("got events from epoll_wait:", event_count)
    for i = 0, < event_count do
      if events[i].data.ptr == nilptr then
        local fd = c_accept(sock, nilptr, nilptr)
        local state = alloc:new(@handler_state)
        if state == nilptr then -- drop connections if allocation fails
          local errmsg = "The server is overloaded, please try again later.\n"
          c_send(fd, errmsg.data, errmsg.size, 0)
          c_close(fd)
          continue
        end
        state.fd = fd
        state.co = coroutine.create(handler)
        state.last_req = handler_req.init
        local ok, events = state:step()
        if ok then
          local fd_event: epoll_event = {
            events = events,
            data = { ptr = (@pointer)(state) }
          }
          c_epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &fd_event)
        else
          local errmsg = "Internal error, please try again later.\n"
          c_send(fd, errmsg.data, errmsg.size, 0)
          c_close(fd)
          alloc:delete()
        end
      else
        print("TODO")
      end
    end
  end
end

local function listen_tcp(address: string, handler: function(): void): void
  local matched, matches = string.match(address, inet_re)
  if matched then
    local c_err: cint = 0

    local s_addr, s_port = matches:unpack(1, 2)
    local addr: uint32
    die_errno(c_inet_pton(AF_INET, (@cstring)(s_addr), &addr) <= 0,
              "bad IPv4 address")
    local i_port = tointeger(s_port)
    assert((i_port >= 0) and (i_port < 65535), "port not within range [0,65536)")
    local port: uint16 = c_htons(i_port)

    local fd = c_socket(AF_INET, SOCK_STREAM, 0)
    die_errno(fd == -1, "couldn't open TCP socket")

    fakeuse(port)
    ##[==[ cemit [[
      struct sockaddr_in sa;
      memset(&sa, 0, sizeof(sa));
      sa.sin_family = AF_INET;
      sa.sin_port = port;
      sa.sin_addr.s_addr = addr;
      c_err = bind(fd, &sa, sizeof(sa));
    ]] ]==]
    die_errno(c_err == -1, "couldn't bind TCP socket")

    listen_sock(fd, handler)
  end
end

listen_tcp("127.0.0.1:1900", function() end)