Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,4 @@
|
||||
## Description
|
||||
|
||||
`picoev` is a V implementation of [picoev](https://github.com/kazuho/picoev),
|
||||
which in turn is "A tiny, lightning fast event loop for network applications".
|
||||
@@ -0,0 +1,6 @@
|
||||
module picoev
|
||||
|
||||
// max_fds is the maximum number of file descriptors that can be managed.
|
||||
// Many sizes depend on it, and some internal arrays are also iterated based on it,
|
||||
// so increasing it a lot can slow down looping :-| .
|
||||
pub const max_fds = 1024
|
||||
@@ -0,0 +1,11 @@
|
||||
module picoev
|
||||
|
||||
// max_fds is the maximum number of file descriptors that can be managed.
|
||||
// Many sizes depend on it, and some internal arrays are also iterated based on it,
|
||||
// so increasing it a lot can slow down looping :-| .
|
||||
// It is higher on windows, because if you start a veb/picoev webservice in a thread,
|
||||
// the returned file descriptors can be higher than 1024 in value, especially if you
|
||||
// also have a webview running in another thread, that also opens its own file descriptors.
|
||||
// Note: this works, because on windows we use select, and select on win32,
|
||||
// is not limited to polling on only 1024 fds.
|
||||
pub const max_fds = 4096
|
||||
@@ -0,0 +1,11 @@
|
||||
module picoev
|
||||
|
||||
@[if picoev_verbose_errors ?]
|
||||
fn elog(msg string) {
|
||||
eprintln(msg)
|
||||
}
|
||||
|
||||
@[if trace_fd ?]
|
||||
fn trace_fd(msg string) {
|
||||
eprintln(msg)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
module picoev
|
||||
|
||||
$if windows {
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
} $else {
|
||||
#include <sys/select.h>
|
||||
}
|
||||
|
||||
pub struct SelectLoop {
|
||||
mut:
|
||||
id int
|
||||
now i64
|
||||
}
|
||||
|
||||
type LoopType = SelectLoop
|
||||
|
||||
// create_select_loop creates a new `SelectLoop` struct with the given `id`.
|
||||
pub fn create_select_loop(id int) !&SelectLoop {
|
||||
return &SelectLoop{
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// updates the events associated with a file descriptor in the event loop.
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) update_events(fd int, events int) int {
|
||||
// check if fd is in range
|
||||
assert fd < max_fds
|
||||
|
||||
pv.file_descriptors[fd].events = u32(events & picoev_readwrite)
|
||||
return 0
|
||||
}
|
||||
|
||||
// performs a single iteration of the select-based event loop.
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) poll_once(max_wait_in_sec int) int {
|
||||
// Initializes sets for read, write, and error events
|
||||
readfds, writefds, errorfds := C.fd_set{}, C.fd_set{}, C.fd_set{}
|
||||
|
||||
// setup
|
||||
C.FD_ZERO(&readfds)
|
||||
C.FD_ZERO(&writefds)
|
||||
C.FD_ZERO(&errorfds)
|
||||
|
||||
mut maxfd := 0
|
||||
|
||||
// finds the maximum file descriptor and adds sockets to the sets `fd_sets`.
|
||||
for target in pv.file_descriptors {
|
||||
if target.loop_id == pv.loop.id {
|
||||
if target.events & picoev_read != 0 {
|
||||
C.FD_SET(target.fd, &readfds)
|
||||
if maxfd < target.fd {
|
||||
maxfd = target.fd
|
||||
}
|
||||
}
|
||||
if target.events & picoev_write != 0 {
|
||||
C.FD_SET(target.fd, &writefds)
|
||||
if maxfd < target.fd {
|
||||
maxfd = target.fd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// select and handle sockets if any
|
||||
tv := C.timeval{
|
||||
tv_sec: u64(max_wait_in_sec)
|
||||
tv_usec: 0
|
||||
}
|
||||
r := C.select(maxfd + 1, &readfds, &writefds, &errorfds, &tv)
|
||||
if r == -1 {
|
||||
// timeout
|
||||
return -1
|
||||
} else if r > 0 {
|
||||
// Iterates through file descriptors and calls their callbacks for triggered events
|
||||
for target in pv.file_descriptors {
|
||||
if target.loop_id == pv.loop.id {
|
||||
mut read_events := 0
|
||||
if C.FD_ISSET(target.fd, &readfds) != 0 {
|
||||
read_events |= picoev_read
|
||||
}
|
||||
if C.FD_ISSET(target.fd, &writefds) != 0 {
|
||||
read_events |= picoev_write
|
||||
}
|
||||
if read_events != 0 {
|
||||
trace_fd('do callback ${target.fd}')
|
||||
// do callback!
|
||||
unsafe { target.cb(target.fd, read_events, &pv) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
module picoev
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/event.h>
|
||||
|
||||
fn C.kevent(i32, changelist voidptr, nchanges i32, eventlist voidptr, nevents i32, timeout &C.timespec) i32
|
||||
fn C.kqueue() i32
|
||||
fn C.EV_SET(kev voidptr, ident i32, filter i16, flags u16, fflags u32, data voidptr, udata voidptr)
|
||||
|
||||
pub struct C.kevent {
|
||||
pub mut:
|
||||
ident int
|
||||
// uintptr_t
|
||||
filter i16
|
||||
flags u16
|
||||
fflags u32
|
||||
data voidptr
|
||||
// intptr_t
|
||||
udata voidptr
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct KqueueLoop {
|
||||
mut:
|
||||
id int
|
||||
now i64
|
||||
kq_id int
|
||||
// -1 if not changed
|
||||
changed_fds int
|
||||
events [1024]C.kevent
|
||||
changelist [256]C.kevent
|
||||
}
|
||||
|
||||
type LoopType = KqueueLoop
|
||||
|
||||
// create_kqueue_loop creates a new kernel event queue with loop_id=`id`.
|
||||
pub fn create_kqueue_loop(id int) !&KqueueLoop {
|
||||
mut loop := &KqueueLoop{
|
||||
id: id
|
||||
}
|
||||
|
||||
loop.kq_id = C.kqueue()
|
||||
if loop.kq_id == -1 {
|
||||
return error('could not create kqueue loop!')
|
||||
}
|
||||
loop.changed_fds = -1
|
||||
return loop
|
||||
}
|
||||
|
||||
// ev_set sets a new `kevent` with file descriptor `index`.
|
||||
@[inline]
|
||||
pub fn (mut pv Picoev) ev_set(index int, operation int, events int) {
|
||||
mut filter := 0
|
||||
if events & picoev_read != 0 {
|
||||
filter |= C.EVFILT_READ
|
||||
}
|
||||
if events & picoev_write != 0 {
|
||||
filter |= C.EVFILT_WRITE
|
||||
}
|
||||
filter = i16(filter)
|
||||
|
||||
C.EV_SET(&pv.loop.changelist[index], pv.loop.changed_fds, filter, operation, 0, 0, 0)
|
||||
}
|
||||
|
||||
// backend_build uses the lower 8 bits to store the old events and the higher 8
|
||||
// bits to store the next file descriptor in `Target.backend`.
|
||||
@[inline]
|
||||
fn backend_build(next_fd int, events u32) int {
|
||||
return int((u32(next_fd) << 8) | (events & 0xff))
|
||||
}
|
||||
|
||||
// get the lower 8 bits.
|
||||
@[inline]
|
||||
fn backend_get_old_events(backend int) int {
|
||||
return backend & 0xff
|
||||
}
|
||||
|
||||
// get the higher 8 bits.
|
||||
@[inline]
|
||||
fn backend_get_next_fd(backend int) int {
|
||||
return backend >> 8
|
||||
}
|
||||
|
||||
// apply pending processes all changes for the file descriptors and updates `loop.changelist`
|
||||
// if `aplly_all` is `true` the changes are immediately applied.
|
||||
fn (mut pv Picoev) apply_pending_changes(apply_all bool) int {
|
||||
mut total, mut nevents := 0, 0
|
||||
|
||||
for pv.loop.changed_fds != -1 {
|
||||
mut target := pv.file_descriptors[pv.loop.changed_fds]
|
||||
old_events := backend_get_old_events(target.backend)
|
||||
if target.events != old_events {
|
||||
// events have been changed
|
||||
if old_events != 0 {
|
||||
pv.ev_set(total, C.EV_DISABLE, old_events)
|
||||
total++
|
||||
}
|
||||
if target.events != 0 {
|
||||
pv.ev_set(total, C.EV_ADD | C.EV_ENABLE, int(target.events))
|
||||
total++
|
||||
}
|
||||
// Apply the changes if the total changes exceed the changelist size
|
||||
if total + 1 >= pv.loop.changelist.len {
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, C.NULL, 0, C.NULL)
|
||||
assert nevents == 0
|
||||
total = 0
|
||||
}
|
||||
}
|
||||
|
||||
pv.loop.changed_fds = backend_get_next_fd(target.backend)
|
||||
target.backend = -1
|
||||
}
|
||||
|
||||
if apply_all && total != 0 {
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, C.NULL, 0, C.NULL)
|
||||
assert nevents == 0
|
||||
total = 0
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) update_events(fd int, events int) int {
|
||||
// check if fd is in range
|
||||
assert fd < max_fds
|
||||
|
||||
mut target := pv.file_descriptors[fd]
|
||||
|
||||
// initialize if adding the fd
|
||||
if events & picoev_add != 0 {
|
||||
target.backend = -1
|
||||
}
|
||||
|
||||
// return if nothing to do
|
||||
if (events == picoev_del && target.backend == -1)
|
||||
|| (events != picoev_del && events & picoev_readwrite == target.events) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// add to changed list if not yet being done
|
||||
if target.backend == -1 {
|
||||
target.backend = backend_build(pv.loop.changed_fds, target.events)
|
||||
pv.loop.changed_fds = fd
|
||||
}
|
||||
|
||||
// update events
|
||||
target.events = u32(events & picoev_readwrite)
|
||||
// apply immediately if is a DELETE
|
||||
if events & picoev_del != 0 {
|
||||
pv.apply_pending_changes(true)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) poll_once(max_wait_in_sec int) int {
|
||||
ts := C.timespec{
|
||||
tv_sec: max_wait_in_sec
|
||||
tv_nsec: 0
|
||||
}
|
||||
|
||||
mut total, mut nevents := 0, 0
|
||||
// apply changes later when the callback is called.
|
||||
total = pv.apply_pending_changes(false)
|
||||
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, &pv.loop.events,
|
||||
pv.loop.events.len, &ts)
|
||||
if nevents == -1 {
|
||||
// the errors we can only rescue
|
||||
assert C.errno == C.EACCES || C.errno == C.EFAULT || C.errno == C.EINTR
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := 0; i < nevents; i++ {
|
||||
event := pv.loop.events[i]
|
||||
target := pv.file_descriptors[event.ident]
|
||||
|
||||
// changelist errors are fatal
|
||||
assert event.flags & C.EV_ERROR == 0
|
||||
|
||||
if pv.loop.id == target.loop_id && event.filter & (C.EVFILT_READ | C.EVFILT_WRITE) != 0 {
|
||||
read_events := match int(event.filter) {
|
||||
C.EVFILT_READ {
|
||||
picoev_read
|
||||
}
|
||||
C.EVFILT_WRITE {
|
||||
picoev_write
|
||||
}
|
||||
else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// do callback!
|
||||
unsafe { target.cb(target.fd, read_events, &pv) }
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
module picoev
|
||||
|
||||
#include <sys/epoll.h>
|
||||
|
||||
$if !musl ? {
|
||||
#include <sys/cdefs.h> // needed for cross compiling to linux
|
||||
}
|
||||
|
||||
fn C.epoll_create(__flags i32) i32
|
||||
fn C.epoll_wait(__epfd i32, __events &C.epoll_event, __maxevents i32, __timeout i32) i32
|
||||
fn C.epoll_ctl(__epfd i32, __op i32, __fd i32, __event &C.epoll_event) i32
|
||||
|
||||
@[typedef]
|
||||
union C.epoll_data {
|
||||
mut:
|
||||
ptr voidptr
|
||||
fd int
|
||||
u32 u32
|
||||
u64 u64
|
||||
}
|
||||
|
||||
@[packed]
|
||||
pub struct C.epoll_event {
|
||||
events u32
|
||||
data C.epoll_data
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct EpollLoop {
|
||||
mut:
|
||||
id int
|
||||
epoll_fd int
|
||||
events [1024]C.epoll_event
|
||||
now i64
|
||||
}
|
||||
|
||||
type LoopType = EpollLoop
|
||||
|
||||
// create_epoll_loop creates a new epoll instance and returns an `EpollLoop` struct with `id`.
|
||||
pub fn create_epoll_loop(id int) !&EpollLoop {
|
||||
mut loop := &EpollLoop{
|
||||
id: id
|
||||
}
|
||||
|
||||
loop.epoll_fd = C.epoll_create(max_fds)
|
||||
if loop.epoll_fd == -1 {
|
||||
return error('could not create epoll loop!')
|
||||
}
|
||||
|
||||
return loop
|
||||
}
|
||||
|
||||
// updates the events associated with a file descriptor in the event loop.
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) update_events(fd int, events int) int {
|
||||
// check if fd is in range
|
||||
assert fd < max_fds
|
||||
|
||||
mut target := pv.file_descriptors[fd]
|
||||
mut ev := C.epoll_event{}
|
||||
|
||||
// fd belongs to loop
|
||||
if events & picoev_del != target.events && target.loop_id != pv.loop.id {
|
||||
return -1
|
||||
}
|
||||
|
||||
if events & picoev_readwrite == target.events {
|
||||
return 0
|
||||
}
|
||||
|
||||
// vfmt off
|
||||
ev.events = u32(
|
||||
(if events & picoev_read != 0 { C.EPOLLIN } else { 0 })
|
||||
|
|
||||
(if events & picoev_write != 0 { C.EPOLLOUT } else { 0 })
|
||||
)
|
||||
// vfmt on
|
||||
ev.data.fd = fd
|
||||
|
||||
if events & picoev_del != 0 {
|
||||
// nothing to do
|
||||
} else if events & picoev_readwrite == 0 {
|
||||
// delete the file if it exists
|
||||
epoll_ret := C.epoll_ctl(pv.loop.epoll_fd, C.EPOLL_CTL_DEL, fd, &ev)
|
||||
|
||||
// check error
|
||||
assert epoll_ret == 0
|
||||
} else {
|
||||
// change settings to 0
|
||||
mut epoll_ret := C.epoll_ctl(pv.loop.epoll_fd, C.EPOLL_CTL_MOD, fd, &ev)
|
||||
if epoll_ret != 0 {
|
||||
// if the file is not present we want to add it
|
||||
assert C.errno == C.ENOENT
|
||||
epoll_ret = C.epoll_ctl(pv.loop.epoll_fd, C.EPOLL_CTL_ADD, fd, &ev)
|
||||
|
||||
// check error
|
||||
assert epoll_ret == 0
|
||||
}
|
||||
}
|
||||
|
||||
// convert to u32?
|
||||
target.events = u32(events)
|
||||
return 0
|
||||
}
|
||||
|
||||
// performs a single iteration of the select-based event loop.
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) poll_once(max_wait_in_sec int) int {
|
||||
nevents := C.epoll_wait(pv.loop.epoll_fd, &pv.loop.events[0], max_fds, max_wait_in_sec * 1000)
|
||||
|
||||
if nevents == -1 {
|
||||
// timeout has occurred
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := 0; i < nevents; i++ {
|
||||
mut event := pv.loop.events[i]
|
||||
target := unsafe { pv.file_descriptors[event.data.fd] }
|
||||
unsafe {
|
||||
assert event.data.fd < max_fds
|
||||
}
|
||||
if pv.loop.id == target.loop_id && target.events & picoev_readwrite != 0 {
|
||||
mut read_events := 0
|
||||
if event.events & u32(C.EPOLLIN) != 0 {
|
||||
read_events |= picoev_read
|
||||
}
|
||||
if event.events & u32(C.EPOLLOUT) != 0 {
|
||||
read_events |= picoev_write
|
||||
}
|
||||
|
||||
if read_events != 0 {
|
||||
// do callback!
|
||||
unsafe { target.cb(event.data.fd, read_events, &pv) }
|
||||
}
|
||||
} else {
|
||||
// defer epoll delete
|
||||
event.events = 0
|
||||
unsafe {
|
||||
C.epoll_ctl(pv.loop.epoll_fd, C.EPOLL_CTL_DEL, event.data.fd, &event)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
module picoev
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/event.h>
|
||||
|
||||
fn C.kevent(i32, changelist voidptr, nchanges i32, eventlist voidptr, nevents i32, timeout &C.timespec) i32
|
||||
fn C.kqueue() i32
|
||||
fn C.EV_SET(kev voidptr, ident i32, filter i16, flags u16, fflags u32, data voidptr, udata voidptr)
|
||||
|
||||
pub struct C.kevent {
|
||||
pub mut:
|
||||
ident int
|
||||
// uintptr_t
|
||||
filter i16
|
||||
flags u16
|
||||
fflags u32
|
||||
data voidptr
|
||||
// intptr_t
|
||||
udata voidptr
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct KqueueLoop {
|
||||
mut:
|
||||
id int
|
||||
now i64
|
||||
kq_id int
|
||||
// -1 if not changed
|
||||
changed_fds int
|
||||
events [1024]C.kevent
|
||||
changelist [256]C.kevent
|
||||
}
|
||||
|
||||
type LoopType = KqueueLoop
|
||||
|
||||
// create_kqueue_loop creates a new kernel event queue with loop_id=`id`.
|
||||
pub fn create_kqueue_loop(id int) !&KqueueLoop {
|
||||
mut loop := &KqueueLoop{
|
||||
id: id
|
||||
}
|
||||
|
||||
loop.kq_id = C.kqueue()
|
||||
if loop.kq_id == -1 {
|
||||
return error('could not create kqueue loop!')
|
||||
}
|
||||
loop.changed_fds = -1
|
||||
return loop
|
||||
}
|
||||
|
||||
// ev_set sets a new `kevent` with file descriptor `index`.
|
||||
@[inline]
|
||||
pub fn (mut pv Picoev) ev_set(index int, operation int, events int) {
|
||||
mut filter := 0
|
||||
if events & picoev_read != 0 {
|
||||
filter |= C.EVFILT_READ
|
||||
}
|
||||
if events & picoev_write != 0 {
|
||||
filter |= C.EVFILT_WRITE
|
||||
}
|
||||
filter = i16(filter)
|
||||
|
||||
C.EV_SET(&pv.loop.changelist[index], pv.loop.changed_fds, filter, operation, 0, 0, 0)
|
||||
}
|
||||
|
||||
// backend_build uses the lower 8 bits to store the old events and the higher 8
|
||||
// bits to store the next file descriptor in `Target.backend`.
|
||||
@[inline]
|
||||
fn backend_build(next_fd int, events u32) int {
|
||||
return int((u32(next_fd) << 8) | (events & 0xff))
|
||||
}
|
||||
|
||||
// get the lower 8 bits.
|
||||
@[inline]
|
||||
fn backend_get_old_events(backend int) int {
|
||||
return backend & 0xff
|
||||
}
|
||||
|
||||
// get the higher 8 bits.
|
||||
@[inline]
|
||||
fn backend_get_next_fd(backend int) int {
|
||||
return backend >> 8
|
||||
}
|
||||
|
||||
// apply pending processes all changes for the file descriptors and updates `loop.changelist`
|
||||
// if `aplly_all` is `true` the changes are immediately applied.
|
||||
fn (mut pv Picoev) apply_pending_changes(apply_all bool) int {
|
||||
mut total, mut nevents := 0, 0
|
||||
|
||||
for pv.loop.changed_fds != -1 {
|
||||
mut target := pv.file_descriptors[pv.loop.changed_fds]
|
||||
old_events := backend_get_old_events(target.backend)
|
||||
if target.events != old_events {
|
||||
// events have been changed
|
||||
if old_events != 0 {
|
||||
pv.ev_set(total, C.EV_DISABLE, old_events)
|
||||
total++
|
||||
}
|
||||
if target.events != 0 {
|
||||
pv.ev_set(total, C.EV_ADD | C.EV_ENABLE, int(target.events))
|
||||
total++
|
||||
}
|
||||
// Apply the changes if the total changes exceed the changelist size
|
||||
if total + 1 >= pv.loop.changelist.len {
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, C.NULL, 0, C.NULL)
|
||||
assert nevents == 0
|
||||
total = 0
|
||||
}
|
||||
}
|
||||
|
||||
pv.loop.changed_fds = backend_get_next_fd(target.backend)
|
||||
target.backend = -1
|
||||
}
|
||||
|
||||
if apply_all && total != 0 {
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, C.NULL, 0, C.NULL)
|
||||
assert nevents == 0
|
||||
total = 0
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// updates the events associated with a file descriptor in the event loop.
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) update_events(fd int, events int) int {
|
||||
// check if fd is in range
|
||||
assert fd < max_fds
|
||||
|
||||
mut target := pv.file_descriptors[fd]
|
||||
|
||||
// initialize if adding the fd
|
||||
if events & picoev_add != 0 {
|
||||
target.backend = -1
|
||||
}
|
||||
|
||||
// return if nothing to do
|
||||
if (events == picoev_del && target.backend == -1)
|
||||
|| (events != picoev_del && events & picoev_readwrite == target.events) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// add to changed list if not yet being done
|
||||
if target.backend == -1 {
|
||||
target.backend = backend_build(pv.loop.changed_fds, target.events)
|
||||
pv.loop.changed_fds = fd
|
||||
}
|
||||
|
||||
// update events
|
||||
target.events = u32(events & picoev_readwrite)
|
||||
// apply immediately if is a DELETE
|
||||
if events & picoev_del != 0 {
|
||||
pv.apply_pending_changes(true)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// performs a single iteration of the select-based event loop.
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) poll_once(max_wait_in_sec int) int {
|
||||
ts := C.timespec{
|
||||
tv_sec: max_wait_in_sec
|
||||
tv_nsec: 0
|
||||
}
|
||||
|
||||
mut total, mut nevents := 0, 0
|
||||
// apply changes later when the callback is called.
|
||||
total = pv.apply_pending_changes(false)
|
||||
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, &pv.loop.events,
|
||||
pv.loop.events.len, &ts)
|
||||
if nevents == -1 {
|
||||
// the errors we can only rescue
|
||||
assert C.errno == C.EACCES || C.errno == C.EFAULT || C.errno == C.EINTR
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := 0; i < nevents; i++ {
|
||||
event := pv.loop.events[i]
|
||||
target := pv.file_descriptors[event.ident]
|
||||
|
||||
// changelist errors are fatal
|
||||
assert event.flags & C.EV_ERROR == 0
|
||||
|
||||
if pv.loop.id == target.loop_id && event.filter & (C.EVFILT_READ | C.EVFILT_WRITE) != 0 {
|
||||
read_events := match int(event.filter) {
|
||||
C.EVFILT_READ {
|
||||
picoev_read
|
||||
}
|
||||
C.EVFILT_WRITE {
|
||||
picoev_write
|
||||
}
|
||||
else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// do callback!
|
||||
unsafe { target.cb(target.fd, read_events, &pv) }
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
module picoev
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/event.h>
|
||||
|
||||
fn C.kevent(i32, changelist voidptr, nchanges i32, eventlist voidptr, nevents i32, timeout &C.timespec) i32
|
||||
fn C.kqueue() i32
|
||||
fn C.EV_SET(kev voidptr, ident i32, filter i16, flags u16, fflags u32, data voidptr, udata voidptr)
|
||||
|
||||
pub struct C.kevent {
|
||||
pub mut:
|
||||
ident int
|
||||
// uintptr_t
|
||||
filter i16
|
||||
flags u16
|
||||
fflags u32
|
||||
data voidptr
|
||||
// intptr_t
|
||||
udata voidptr
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct KqueueLoop {
|
||||
mut:
|
||||
id int
|
||||
now i64
|
||||
kq_id int
|
||||
// -1 if not changed
|
||||
changed_fds int
|
||||
events [1024]C.kevent
|
||||
changelist [256]C.kevent
|
||||
}
|
||||
|
||||
type LoopType = KqueueLoop
|
||||
|
||||
// create_kqueue_loop creates a new kernel event queue with loop_id=`id`.
|
||||
pub fn create_kqueue_loop(id int) !&KqueueLoop {
|
||||
mut loop := &KqueueLoop{
|
||||
id: id
|
||||
}
|
||||
|
||||
loop.kq_id = C.kqueue()
|
||||
if loop.kq_id == -1 {
|
||||
return error('could not create kqueue loop!')
|
||||
}
|
||||
loop.changed_fds = -1
|
||||
return loop
|
||||
}
|
||||
|
||||
// ev_set sets a new `kevent` with file descriptor `index`.
|
||||
@[inline]
|
||||
pub fn (mut pv Picoev) ev_set(index int, operation int, events int) {
|
||||
mut filter := 0
|
||||
if events & picoev_read != 0 {
|
||||
filter |= C.EVFILT_READ
|
||||
}
|
||||
if events & picoev_write != 0 {
|
||||
filter |= C.EVFILT_WRITE
|
||||
}
|
||||
filter = i16(filter)
|
||||
|
||||
C.EV_SET(&pv.loop.changelist[index], pv.loop.changed_fds, filter, operation, 0, 0, 0)
|
||||
}
|
||||
|
||||
// backend_build uses the lower 8 bits to store the old events and the higher 8
|
||||
// bits to store the next file descriptor in `Target.backend`.
|
||||
@[inline]
|
||||
fn backend_build(next_fd int, events u32) int {
|
||||
return int((u32(next_fd) << 8) | (events & 0xff))
|
||||
}
|
||||
|
||||
// get the lower 8 bits
|
||||
@[inline]
|
||||
fn backend_get_old_events(backend int) int {
|
||||
return backend & 0xff
|
||||
}
|
||||
|
||||
// get the higher 8 bits
|
||||
@[inline]
|
||||
fn backend_get_next_fd(backend int) int {
|
||||
return backend >> 8
|
||||
}
|
||||
|
||||
// apply pending processes all changes for the file descriptors and updates `loop.changelist`
|
||||
// if `aplly_all` is `true` the changes are immediately applied.
|
||||
fn (mut pv Picoev) apply_pending_changes(apply_all bool) int {
|
||||
mut total, mut nevents := 0, 0
|
||||
|
||||
for pv.loop.changed_fds != -1 {
|
||||
mut target := pv.file_descriptors[pv.loop.changed_fds]
|
||||
old_events := backend_get_old_events(target.backend)
|
||||
if target.events != old_events {
|
||||
// events have been changed
|
||||
if old_events != 0 {
|
||||
pv.ev_set(total, C.EV_DISABLE, old_events)
|
||||
total++
|
||||
}
|
||||
if target.events != 0 {
|
||||
pv.ev_set(total, C.EV_ADD | C.EV_ENABLE, int(target.events))
|
||||
total++
|
||||
}
|
||||
// Apply the changes if the total changes exceed the changelist size
|
||||
if total + 1 >= pv.loop.changelist.len {
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, C.NULL, 0, C.NULL)
|
||||
assert nevents == 0
|
||||
total = 0
|
||||
}
|
||||
}
|
||||
|
||||
pv.loop.changed_fds = backend_get_next_fd(target.backend)
|
||||
target.backend = -1
|
||||
}
|
||||
|
||||
if apply_all && total != 0 {
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, C.NULL, 0, C.NULL)
|
||||
assert nevents == 0
|
||||
total = 0
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) update_events(fd int, events int) int {
|
||||
// check if fd is in range
|
||||
assert fd < max_fds
|
||||
|
||||
mut target := pv.file_descriptors[fd]
|
||||
|
||||
// initialize if adding the fd
|
||||
if events & picoev_add != 0 {
|
||||
target.backend = -1
|
||||
}
|
||||
|
||||
// return if nothing to do
|
||||
if (events == picoev_del && target.backend == -1)
|
||||
|| (events != picoev_del && events & picoev_readwrite == target.events) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// add to changed list if not yet being done
|
||||
if target.backend == -1 {
|
||||
target.backend = backend_build(pv.loop.changed_fds, target.events)
|
||||
pv.loop.changed_fds = fd
|
||||
}
|
||||
|
||||
// update events
|
||||
target.events = u32(events & picoev_readwrite)
|
||||
// apply immediately if is a DELETE
|
||||
if events & picoev_del != 0 {
|
||||
pv.apply_pending_changes(true)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) poll_once(max_wait_in_sec int) int {
|
||||
ts := C.timespec{
|
||||
tv_sec: max_wait_in_sec
|
||||
tv_nsec: 0
|
||||
}
|
||||
|
||||
mut total, mut nevents := 0, 0
|
||||
// apply changes later when the callback is called.
|
||||
total = pv.apply_pending_changes(false)
|
||||
|
||||
nevents = C.kevent(pv.loop.kq_id, &pv.loop.changelist, total, &pv.loop.events,
|
||||
pv.loop.events.len, &ts)
|
||||
if nevents == -1 {
|
||||
// the errors we can only rescue
|
||||
assert C.errno == C.EACCES || C.errno == C.EFAULT || C.errno == C.EINTR
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := 0; i < nevents; i++ {
|
||||
event := pv.loop.events[i]
|
||||
target := pv.file_descriptors[event.ident]
|
||||
|
||||
// changelist errors are fatal
|
||||
assert event.flags & C.EV_ERROR == 0
|
||||
|
||||
if pv.loop.id == target.loop_id && event.filter & (C.EVFILT_READ | C.EVFILT_WRITE) != 0 {
|
||||
read_events := match int(event.filter) {
|
||||
C.EVFILT_READ {
|
||||
picoev_read
|
||||
}
|
||||
C.EVFILT_WRITE {
|
||||
picoev_write
|
||||
}
|
||||
else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// do callback!
|
||||
unsafe { target.cb(target.fd, read_events, &pv) }
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
module picoev
|
||||
|
||||
#include <sys/epoll.h>
|
||||
|
||||
$if !musl ? {
|
||||
#include <sys/cdefs.h> // needed for cross compiling to linux
|
||||
}
|
||||
|
||||
fn C.epoll_create(__flags i32) i32
|
||||
fn C.epoll_wait(__epfd i32, __events &C.epoll_event, __maxevents i32, __timeout i32) i32
|
||||
fn C.epoll_ctl(__epfd i32, __op i32, __fd i32, __event &C.epoll_event) i32
|
||||
|
||||
@[typedef]
|
||||
union C.epoll_data {
|
||||
mut:
|
||||
ptr voidptr
|
||||
fd int
|
||||
u32 u32
|
||||
u64 u64
|
||||
}
|
||||
|
||||
@[packed]
|
||||
pub struct C.epoll_event {
|
||||
events u32
|
||||
data C.epoll_data
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct EpollLoop {
|
||||
mut:
|
||||
id int
|
||||
epoll_fd int
|
||||
events [1024]C.epoll_event
|
||||
now i64
|
||||
}
|
||||
|
||||
type LoopType = EpollLoop
|
||||
|
||||
// create_epoll_loop creates a new epoll instance and returns an `EpollLoop` struct with `id`.
|
||||
pub fn create_epoll_loop(id int) !&EpollLoop {
|
||||
mut loop := &EpollLoop{
|
||||
id: id
|
||||
}
|
||||
|
||||
loop.epoll_fd = C.epoll_create(max_fds)
|
||||
if loop.epoll_fd == -1 {
|
||||
return error('could not create epoll loop!')
|
||||
}
|
||||
|
||||
return loop
|
||||
}
|
||||
|
||||
// updates the events associated with a file descriptor in the event loop.
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) update_events(fd int, events int) int {
|
||||
// check if fd is in range
|
||||
assert fd < max_fds
|
||||
|
||||
mut target := pv.file_descriptors[fd]
|
||||
mut ev := C.epoll_event{}
|
||||
|
||||
// fd belongs to loop
|
||||
if events & picoev_del != target.events && target.loop_id != pv.loop.id {
|
||||
return -1
|
||||
}
|
||||
|
||||
if events & picoev_readwrite == target.events {
|
||||
return 0
|
||||
}
|
||||
|
||||
// vfmt off
|
||||
ev.events = u32(
|
||||
(if events & picoev_read != 0 { C.EPOLLIN } else { 0 })
|
||||
|
|
||||
(if events & picoev_write != 0 { C.EPOLLOUT } else { 0 })
|
||||
)
|
||||
// vfmt on
|
||||
ev.data.fd = fd
|
||||
|
||||
if events & picoev_del != 0 {
|
||||
// nothing to do
|
||||
} else if events & picoev_readwrite == 0 {
|
||||
// delete the file if it exists
|
||||
epoll_ret := C.epoll_ctl(pv.loop.epoll_fd, C.EPOLL_CTL_DEL, fd, &ev)
|
||||
|
||||
// check error
|
||||
assert epoll_ret == 0
|
||||
} else {
|
||||
// change settings to 0
|
||||
mut epoll_ret := C.epoll_ctl(pv.loop.epoll_fd, C.EPOLL_CTL_MOD, fd, &ev)
|
||||
if epoll_ret != 0 {
|
||||
// if the file is not present we want to add it
|
||||
assert C.errno == C.ENOENT
|
||||
epoll_ret = C.epoll_ctl(pv.loop.epoll_fd, C.EPOLL_CTL_ADD, fd, &ev)
|
||||
|
||||
// check error
|
||||
assert epoll_ret == 0
|
||||
}
|
||||
}
|
||||
|
||||
// convert to u32?
|
||||
target.events = u32(events)
|
||||
return 0
|
||||
}
|
||||
|
||||
// performs a single iteration of the select-based event loop.
|
||||
@[direct_array_access]
|
||||
fn (mut pv Picoev) poll_once(max_wait_in_sec int) int {
|
||||
nevents := C.epoll_wait(pv.loop.epoll_fd, &pv.loop.events[0], max_fds, max_wait_in_sec * 1000)
|
||||
|
||||
if nevents == -1 {
|
||||
// timeout has occurred
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := 0; i < nevents; i++ {
|
||||
mut event := pv.loop.events[i]
|
||||
target := unsafe { pv.file_descriptors[event.data.fd] }
|
||||
unsafe {
|
||||
assert event.data.fd < max_fds
|
||||
}
|
||||
if pv.loop.id == target.loop_id && target.events & picoev_readwrite != 0 {
|
||||
mut read_events := 0
|
||||
if event.events & u32(C.EPOLLIN) != 0 {
|
||||
read_events |= picoev_read
|
||||
}
|
||||
if event.events & u32(C.EPOLLOUT) != 0 {
|
||||
read_events |= picoev_write
|
||||
}
|
||||
|
||||
if read_events != 0 {
|
||||
// do callback!
|
||||
unsafe { target.cb(event.data.fd, read_events, &pv) }
|
||||
}
|
||||
} else {
|
||||
// defer epoll delete
|
||||
event.events = 0
|
||||
unsafe {
|
||||
C.epoll_ctl(pv.loop.epoll_fd, C.EPOLL_CTL_DEL, event.data.fd, &event)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
module picoev
|
||||
|
||||
import net
|
||||
import pico_http_parser
|
||||
import time
|
||||
|
||||
// maximum size of the event queue.
|
||||
pub const max_queue = 4096
|
||||
|
||||
// event for incoming data ready to be read on a socket.
|
||||
pub const picoev_read = 1
|
||||
|
||||
// event for socket ready for writing.
|
||||
pub const picoev_write = 2
|
||||
|
||||
// event indicating a timeout has occurred.
|
||||
pub const picoev_timeout = 4
|
||||
|
||||
// flag for adding a file descriptor to the event loop.
|
||||
pub const picoev_add = 0x40000000
|
||||
|
||||
// flag for removing a file descriptor from the event loop.
|
||||
pub const picoev_del = 0x20000000
|
||||
|
||||
// event read/write.
|
||||
pub const picoev_readwrite = 3
|
||||
|
||||
// Target is a data representation of everything that needs to be associated with a single file descriptor (connection).
|
||||
pub struct Target {
|
||||
pub mut:
|
||||
fd int // file descriptor
|
||||
loop_id int = -1
|
||||
events u32
|
||||
cb fn (int, int, voidptr) = unsafe { nil }
|
||||
// used internally by the kqueue implementation
|
||||
backend int
|
||||
}
|
||||
|
||||
// Config configures the Picoev instance with server settings and callbacks.
|
||||
pub struct Config {
|
||||
pub:
|
||||
port int = 8080
|
||||
cb fn (voidptr, pico_http_parser.Request, mut pico_http_parser.Response) = unsafe { nil }
|
||||
err_cb fn (voidptr, pico_http_parser.Request, mut pico_http_parser.Response, IError) = default_error_callback
|
||||
raw_cb fn (mut Picoev, int, int) = unsafe { nil }
|
||||
user_data voidptr = unsafe { nil }
|
||||
timeout_secs int = 8
|
||||
max_headers int = 100
|
||||
max_read int = 4096
|
||||
max_write int = 8192
|
||||
family net.AddrFamily = .ip6
|
||||
host string
|
||||
}
|
||||
|
||||
// Core structure for managing the event loop and connections.
|
||||
// Contains event loop, file descriptor table, timeouts, buffers, and configuration.
|
||||
@[heap]
|
||||
pub struct Picoev {
|
||||
cb fn (voidptr, pico_http_parser.Request, mut pico_http_parser.Response) = unsafe { nil }
|
||||
error_callback fn (voidptr, pico_http_parser.Request, mut pico_http_parser.Response, IError) = default_error_callback
|
||||
raw_callback fn (mut Picoev, int, int) = unsafe { nil }
|
||||
|
||||
timeout_secs int
|
||||
max_headers int = 100
|
||||
max_read int = 4096
|
||||
max_write int = 8192
|
||||
mut:
|
||||
loop &LoopType = unsafe { nil }
|
||||
file_descriptors [4096]&Target // TODO: use max_fds here, instead of the hardcoded size, when the compiler allows it
|
||||
timeouts map[int]i64
|
||||
num_loops int
|
||||
|
||||
buf &u8 = unsafe { nil }
|
||||
idx [max_fds]int
|
||||
out &u8 = unsafe { nil }
|
||||
|
||||
date string
|
||||
pub:
|
||||
user_data voidptr = unsafe { nil }
|
||||
}
|
||||
|
||||
// init fills the `file_descriptors` array.
|
||||
pub fn (mut pv Picoev) init() {
|
||||
// assert max_fds > 0
|
||||
pv.num_loops = 0
|
||||
for i in 0 .. max_fds {
|
||||
pv.file_descriptors[i] = &Target{}
|
||||
}
|
||||
}
|
||||
|
||||
// add a file descriptor to the event loop.
|
||||
@[direct_array_access]
|
||||
pub fn (mut pv Picoev) add(fd int, events int, timeout int, callback voidptr) int {
|
||||
if pv == unsafe { nil } || fd < 0 || fd >= max_fds {
|
||||
return -1 // Invalid arguments
|
||||
}
|
||||
mut target := pv.file_descriptors[fd]
|
||||
target.fd = fd
|
||||
target.cb = callback
|
||||
target.loop_id = pv.loop.id
|
||||
target.events = 0
|
||||
if pv.update_events(fd, events | picoev_add) != 0 {
|
||||
if pv.delete(fd) != 0 {
|
||||
elog('Error during del')
|
||||
}
|
||||
return -1
|
||||
}
|
||||
pv.set_timeout(fd, timeout)
|
||||
return 0
|
||||
}
|
||||
|
||||
// remove a file descriptor from the event loop.
|
||||
@[direct_array_access]
|
||||
pub fn (mut pv Picoev) delete(fd int) int {
|
||||
if fd < 0 || fd >= max_fds {
|
||||
return -1 // Invalid fd
|
||||
}
|
||||
mut target := pv.file_descriptors[fd]
|
||||
trace_fd('remove ${fd}')
|
||||
if pv.update_events(fd, picoev_del) != 0 {
|
||||
elog('Error during update_events. event: `picoev.picoev_del`')
|
||||
return -1
|
||||
}
|
||||
pv.set_timeout(fd, 0)
|
||||
target.loop_id = -1
|
||||
target.fd = 0
|
||||
target.cb = unsafe { nil } // Clear callback to prevent accidental invocations
|
||||
return 0
|
||||
}
|
||||
|
||||
fn (mut pv Picoev) loop_once(max_wait_in_sec int) int {
|
||||
pv.loop.now = get_time()
|
||||
if pv.poll_once(max_wait_in_sec) != 0 {
|
||||
$if !windows {
|
||||
if C.errno == net.error_eintr {
|
||||
// Signal-driven wakeups are transient. The caller should keep serving,
|
||||
// instead of spinning on a logged "error" until the signal source stops.
|
||||
return 0
|
||||
}
|
||||
}
|
||||
elog('Error during poll_once')
|
||||
return -1
|
||||
}
|
||||
if max_wait_in_sec == 0 {
|
||||
// If no waiting, skip timeout handling for potential performance optimization
|
||||
return 0
|
||||
}
|
||||
// Update loop start time again if waiting occurred
|
||||
pv.loop.now = get_time()
|
||||
pv.handle_timeout()
|
||||
return 0
|
||||
}
|
||||
|
||||
// set_timeout sets the timeout in seconds for a file descriptor. If a timeout occurs
|
||||
// the file descriptors target callback is called with a timeout event.
|
||||
@[direct_array_access; inline]
|
||||
fn (mut pv Picoev) set_timeout(fd int, secs int) {
|
||||
assert fd < max_fds
|
||||
if secs == 0 {
|
||||
pv.timeouts.delete(fd)
|
||||
} else {
|
||||
pv.timeouts[fd] = pv.loop.now + secs
|
||||
}
|
||||
}
|
||||
|
||||
// handle_timeout loops over all file descriptors and removes them from the loop
|
||||
// if they are timed out. Also the file descriptors target callback is called with a
|
||||
// timeout event.
|
||||
@[direct_array_access; inline]
|
||||
fn (mut pv Picoev) handle_timeout() {
|
||||
mut to_remove := []int{}
|
||||
for fd, timeout in pv.timeouts {
|
||||
if timeout <= pv.loop.now {
|
||||
to_remove << fd
|
||||
}
|
||||
}
|
||||
for fd in to_remove {
|
||||
target := pv.file_descriptors[fd]
|
||||
assert target.loop_id == pv.loop.id
|
||||
pv.timeouts.delete(fd)
|
||||
unsafe { target.cb(fd, picoev_timeout, &pv) }
|
||||
}
|
||||
}
|
||||
|
||||
// accept_callback accepts a new connection from `listen_fd` and adds it to the event loop.
|
||||
fn accept_callback(listen_fd int, _events int, cb_arg voidptr) {
|
||||
mut pv := unsafe { &Picoev(cb_arg) }
|
||||
accepted_fd := accept(listen_fd)
|
||||
if accepted_fd == -1 {
|
||||
if fatal_socket_error(accepted_fd) == false {
|
||||
return
|
||||
}
|
||||
elog('Error during accept')
|
||||
return
|
||||
}
|
||||
if accepted_fd >= max_fds {
|
||||
// should never happen
|
||||
elog('Error during accept, accepted_fd >= max_fd')
|
||||
close_socket(accepted_fd)
|
||||
return
|
||||
}
|
||||
trace_fd('accept ${accepted_fd}')
|
||||
setup_sock(accepted_fd) or {
|
||||
elog('setup_sock failed, fd: ${accepted_fd}, listen_fd: ${listen_fd}, err: ${err.code()}')
|
||||
pv.error_callback(pv.user_data, pico_http_parser.Request{}, mut
|
||||
&pico_http_parser.Response{}, err)
|
||||
close_socket(accepted_fd) // Close fd on failure
|
||||
return
|
||||
}
|
||||
pv.add(accepted_fd, picoev_read, pv.timeout_secs, raw_callback)
|
||||
}
|
||||
|
||||
// close_conn closes the socket `fd` and removes it from the loop.
|
||||
@[inline]
|
||||
pub fn (mut pv Picoev) close_conn(fd int) {
|
||||
if pv.delete(fd) != 0 {
|
||||
elog('Error during del')
|
||||
}
|
||||
close_socket(fd)
|
||||
}
|
||||
|
||||
// raw_callback handles raw events (read, write, timeout) for a file descriptor.
|
||||
@[direct_array_access]
|
||||
fn raw_callback(fd int, events int, context voidptr) {
|
||||
mut pv := unsafe { &Picoev(context) }
|
||||
defer {
|
||||
pv.idx[fd] = 0
|
||||
}
|
||||
if events & picoev_timeout != 0 {
|
||||
trace_fd('timeout ${fd}')
|
||||
if !isnil(pv.raw_callback) {
|
||||
pv.raw_callback(mut pv, fd, events)
|
||||
return
|
||||
}
|
||||
pv.close_conn(fd)
|
||||
return
|
||||
} else if events & picoev_read != 0 {
|
||||
pv.set_timeout(fd, pv.timeout_secs)
|
||||
if !isnil(pv.raw_callback) {
|
||||
pv.raw_callback(mut pv, fd, events)
|
||||
return
|
||||
}
|
||||
mut request_buffer := pv.buf
|
||||
unsafe {
|
||||
request_buffer += fd * pv.max_read // pointer magic
|
||||
}
|
||||
mut req := pico_http_parser.Request{}
|
||||
// Response init
|
||||
mut response_buffer := pv.out
|
||||
unsafe {
|
||||
response_buffer += fd * pv.max_write // pointer magic
|
||||
}
|
||||
mut res := pico_http_parser.Response{
|
||||
fd: fd
|
||||
buf_start: response_buffer
|
||||
buf: response_buffer
|
||||
date: pv.date.str
|
||||
}
|
||||
for {
|
||||
// Request parsing loop
|
||||
r := req_read(fd, request_buffer, pv.max_read, pv.idx[fd]) // Get data from socket
|
||||
if r == 0 {
|
||||
// connection closed by peer
|
||||
pv.close_conn(fd)
|
||||
return
|
||||
} else if r == -1 {
|
||||
if fatal_socket_error(fd) == false {
|
||||
return
|
||||
}
|
||||
elog('Error during req_read')
|
||||
// fatal error
|
||||
pv.close_conn(fd)
|
||||
return
|
||||
}
|
||||
pv.idx[fd] += r
|
||||
mut s := unsafe { tos(request_buffer, pv.idx[fd]) }
|
||||
pret := req.parse_request(s) or {
|
||||
// Parse error
|
||||
pv.error_callback(pv.user_data, req, mut &res, err)
|
||||
return
|
||||
}
|
||||
if pret > 0 { // Success
|
||||
break
|
||||
}
|
||||
assert pret == -2
|
||||
// request is incomplete, continue the loop
|
||||
if pv.idx[fd] == sizeof(request_buffer) {
|
||||
pv.error_callback(pv.user_data, req, mut &res, error('RequestIsTooLongError'))
|
||||
return
|
||||
}
|
||||
}
|
||||
// Callback (should call .end() itself)
|
||||
pv.cb(pv.user_data, req, mut &res)
|
||||
} else if events & picoev_write != 0 {
|
||||
pv.set_timeout(fd, pv.timeout_secs)
|
||||
if !isnil(pv.raw_callback) {
|
||||
pv.raw_callback(mut pv, fd, events)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_error_callback(_data voidptr, _req pico_http_parser.Request, mut res pico_http_parser.Response, error IError) {
|
||||
elog('picoev: ${error}')
|
||||
res.end()
|
||||
}
|
||||
|
||||
// new creates a `Picoev` struct and initializes the main loop.
|
||||
pub fn new(config Config) !&Picoev {
|
||||
listening_socket_fd := listen(config) or {
|
||||
elog('Error during listen: ${err}')
|
||||
return err
|
||||
}
|
||||
mut pv := &Picoev{
|
||||
num_loops: 1
|
||||
cb: config.cb
|
||||
error_callback: config.err_cb
|
||||
raw_callback: config.raw_cb
|
||||
user_data: config.user_data
|
||||
timeout_secs: config.timeout_secs
|
||||
max_headers: config.max_headers
|
||||
max_read: config.max_read
|
||||
max_write: config.max_write
|
||||
}
|
||||
if isnil(pv.raw_callback) {
|
||||
pv.buf = unsafe { malloc_noscan(max_fds * config.max_read + 1) }
|
||||
pv.out = unsafe { malloc_noscan(max_fds * config.max_write + 1) }
|
||||
}
|
||||
// epoll on linux
|
||||
// kqueue on macos and bsd
|
||||
// select on windows and others
|
||||
$if linux || termux {
|
||||
pv.loop = create_epoll_loop(0) or { panic(err) }
|
||||
} $else $if freebsd || macos || openbsd {
|
||||
pv.loop = create_kqueue_loop(0) or { panic(err) }
|
||||
} $else {
|
||||
pv.loop = create_select_loop(0) or { panic(err) }
|
||||
}
|
||||
if pv.loop == unsafe { nil } {
|
||||
elog('Failed to create loop')
|
||||
close_socket(listening_socket_fd)
|
||||
return unsafe { nil }
|
||||
}
|
||||
pv.init()
|
||||
pv.add(listening_socket_fd, picoev_read, 0, accept_callback)
|
||||
return pv
|
||||
}
|
||||
|
||||
// serve starts the event loop for accepting new connections.
|
||||
// See also picoev.new().
|
||||
pub fn (mut pv Picoev) serve() {
|
||||
spawn update_date_string(mut pv)
|
||||
for {
|
||||
pv.loop_once(1)
|
||||
}
|
||||
}
|
||||
|
||||
// update_date updates the date field of the Picoev instance every second for HTTP headers.
|
||||
fn update_date_string(mut pv Picoev) {
|
||||
for {
|
||||
// get GMT (UTC) time for the HTTP Date header
|
||||
gmt := time.utc()
|
||||
pv.date = gmt.http_header_string()
|
||||
time.sleep(time.second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
module picoev
|
||||
|
||||
import os
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
fn C.alarm(seconds u32) u32
|
||||
|
||||
fn ignore_test_signal(_ os.Signal) {}
|
||||
|
||||
fn test_loop_once_ignores_eintr() {
|
||||
mut pv := &Picoev{}
|
||||
pv.loop = create_epoll_loop(0) or { panic(err) }
|
||||
pv.init()
|
||||
prev_handler := os.signal_opt(.alrm, ignore_test_signal) or { panic(err) }
|
||||
defer {
|
||||
C.alarm(0)
|
||||
os.signal_opt(.alrm, prev_handler) or { panic(err) }
|
||||
}
|
||||
C.alarm(1)
|
||||
assert pv.loop_once(2) == 0
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
module picoev
|
||||
|
||||
fn test_if_all_file_descriptors_are_properly_initialized() {
|
||||
mut pv := &Picoev{}
|
||||
pv.init()
|
||||
|
||||
for i in 0 .. max_fds {
|
||||
assert unsafe { pv.file_descriptors[i] } != unsafe { nil }
|
||||
assert unsafe { pv.file_descriptors[i].loop_id } == -1
|
||||
assert unsafe { pv.file_descriptors[i].fd } == 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
module picoev
|
||||
|
||||
import net
|
||||
import pico_http_parser
|
||||
|
||||
#include <errno.h>
|
||||
$if windows {
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
} $else $if freebsd || macos {
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
} $else {
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/resource.h>
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn get_time() i64 {
|
||||
// time.now() is slow
|
||||
return i64(C.time(C.NULL))
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn accept(fd int) int {
|
||||
return C.accept(fd, 0, 0)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn close_socket(fd int) {
|
||||
trace_fd('close ${fd}')
|
||||
$if windows {
|
||||
C.closesocket(fd)
|
||||
} $else {
|
||||
C.close(fd)
|
||||
}
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn setup_sock(fd int) ! {
|
||||
flag := 1
|
||||
if C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_NODELAY, &flag, sizeof(int)) < 0 {
|
||||
return error('setup_sock.setup_sock failed')
|
||||
}
|
||||
$if freebsd {
|
||||
if C.fcntl(fd, C.F_SETFL, C.SOCK_NONBLOCK) != 0 {
|
||||
return error('fcntl failed')
|
||||
}
|
||||
} $else $if windows {
|
||||
non_blocking_mode := u32(1)
|
||||
if C.ioctlsocket(fd, C.FIONBIO, &non_blocking_mode) == C.SOCKET_ERROR {
|
||||
return error('icotlsocket failed')
|
||||
}
|
||||
} $else {
|
||||
// linux and macos
|
||||
if C.fcntl(fd, C.F_SETFL, C.O_NONBLOCK) != 0 {
|
||||
return error('fcntl failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn req_read(fd int, buffer &u8, max_len int, offset int) int {
|
||||
// use `recv` instead of `read` for windows compatibility
|
||||
return unsafe { C.recv(fd, buffer + offset, max_len - offset, 0) }
|
||||
}
|
||||
|
||||
fn fatal_socket_error(fd int) bool {
|
||||
if C.errno == C.EAGAIN {
|
||||
// try again later
|
||||
return false
|
||||
}
|
||||
$if windows {
|
||||
if C.errno == C.WSAEWOULDBLOCK {
|
||||
// try again later
|
||||
return false
|
||||
}
|
||||
} $else {
|
||||
if C.errno == C.EWOULDBLOCK {
|
||||
// try again later
|
||||
return false
|
||||
}
|
||||
}
|
||||
trace_fd('fatal error ${fd}: ${C.errno}')
|
||||
return true
|
||||
}
|
||||
|
||||
// listen creates a listening tcp socket and returns its file descriptor.
|
||||
fn listen(config Config) !int {
|
||||
// not using the `net` modules sockets, because not all socket options are defined
|
||||
fd := C.socket(i32(config.family), i32(net.SocketType.tcp), 0)
|
||||
if fd == -1 {
|
||||
return error('Failed to create socket')
|
||||
}
|
||||
trace_fd('listen: ${fd}')
|
||||
// Setting flags for socket
|
||||
flag := 1
|
||||
flag_zero := 0
|
||||
net.socket_error(C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEADDR, &flag, sizeof(int)))!
|
||||
if config.family == .ip6 {
|
||||
// set socket to dualstack so connections to both ipv4 and ipv6 addresses
|
||||
// can be accepted
|
||||
net.socket_error(C.setsockopt(fd, C.IPPROTO_IPV6, C.IPV6_V6ONLY, &flag_zero, sizeof(int)))!
|
||||
}
|
||||
$if linux || termux {
|
||||
// epoll socket options
|
||||
net.socket_error(C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEPORT, &flag, sizeof(int)))!
|
||||
net.socket_error(C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_QUICKACK, &flag, sizeof(int)))!
|
||||
$if !support_wsl1 ? {
|
||||
net.socket_error(C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_DEFER_ACCEPT,
|
||||
&config.timeout_secs, sizeof(int)))!
|
||||
queue_len := max_queue
|
||||
net.socket_error(C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_FASTOPEN, &queue_len,
|
||||
sizeof(int)))!
|
||||
}
|
||||
}
|
||||
// addr settings
|
||||
saddr := '${config.host}:${config.port}'
|
||||
addrs := net.resolve_addrs(saddr, config.family, .tcp) or {
|
||||
panic('Error while resolving `${saddr}`, err: ${err}')
|
||||
}
|
||||
addr := addrs[0]
|
||||
alen := addr.len()
|
||||
net.socket_error_message(C.bind(fd, voidptr(&addr), alen), 'binding to ${saddr} failed')!
|
||||
net.socket_error_message(C.listen(fd, C.SOMAXCONN),
|
||||
'listening on ${saddr} with maximum backlog pending queue of ${C.SOMAXCONN}, failed')!
|
||||
setup_sock(fd) or {
|
||||
config.err_cb(config.user_data, pico_http_parser.Request{}, mut
|
||||
&pico_http_parser.Response{}, err)
|
||||
}
|
||||
return fd
|
||||
}
|
||||
Reference in new issue
Block a user