├── LICENSE ├── README.md ├── coro.nim ├── eventqueue.nim ├── io.nim ├── main.nim └── ucontext.nim /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ico Doornekamp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This is a little proof-of-concept prject to see if I can get Lua-style 3 | coroutines semantics in Nim, and use this to build an alternative async 4 | implementation. It's built on very low level primitives only: the `posix` lib 5 | for sockets and `poll()`, and a small wrapper around the posix `ucontext` 6 | functions. 7 | 8 | There's a few moving parts in this project: 9 | 10 | - coro.nim: This implements simple coroutines based on ucontext. This are 11 | basically Lua-style coroutines, but because of Nims static typing it is hard 12 | to implement the Lua way of passing data through `yield()` and `resume()`. 13 | For now I've chosen not to pass data at all, as there are enough other ways 14 | to do that outside of the core coroutine 15 | 16 | - evq.nim: This is a very basic and naive event loop implementation: register 17 | file descriptors with read or write events and a proc, and your proc will 18 | be called back when the file descriptor is ready. 19 | 20 | - io.nim: Here the above two modules come together to create very friendly 21 | async I/O. Look at the `waitForFd()` proc to see what is happening. 22 | 23 | - main.nim: This example creates a listening TCP socket which can handle 24 | multiple clients, which are all run inside coroutines. 25 | 26 | Note that the curent coro implementation confuses Nim's GC, run with `--gc:arc`! 27 | 28 | -------------------------------------------------------------------------------- /coro.nim: -------------------------------------------------------------------------------- 1 | import ucontext 2 | 3 | const stackSize = 32768 4 | 5 | type 6 | 7 | CoroException* = object of CatchableError 8 | 9 | CoroStatus* = enum 10 | csRunning, # running, i.e. it called status() 11 | csSuspended, # suspended into a jield() 12 | csNormal, # active but not running (resumed another coro) 13 | csDead # finished or stopped with an exception 14 | 15 | Coro* = ref object 16 | ctx: ucontext_t 17 | stack: array[stackSize, uint8] 18 | fn: CoroFn 19 | status*: CoroStatus 20 | caller: Coro # The coroutine resuming us 21 | task: TaskBase 22 | 23 | CoroFn* = proc(t: TaskBase) 24 | 25 | TaskBase* = ref object of RootObj 26 | 27 | var coroMain {.threadvar.}: Coro # The "main" coroutine, which is actually not a coroutine 28 | var coroCur {.threadVar.}: Coro # The current active coroutine. 29 | 30 | proc jield*() 31 | proc resume*(coro: Coro) 32 | 33 | 34 | # makecontext() target 35 | 36 | proc schedule(coro: Coro) {.cdecl.} = 37 | coro.fn(coro.task) 38 | coro.status = csDead 39 | jield() 40 | 41 | 42 | proc newCoro*(fn: CoroFn, task=TaskBase(), start=true): Coro {.discardable.} = 43 | ## Create a new coroutine with body `fn`. If `start` is true the coroutine 44 | ## will be executed right away 45 | let coro = Coro(fn: fn, status: csSuspended, task: task) 46 | coro.ctx.uc_stack.ss_sp = coro.stack[0].addr 47 | coro.ctx.uc_stack.ss_size = coro.stack.len 48 | 49 | let r = getcontext(coro.ctx) 50 | doAssert(r == 0) 51 | makecontext(coro.ctx, schedule, 1, coro); 52 | 53 | if start: 54 | coro.resume() 55 | 56 | return coro 57 | 58 | 59 | proc resume*(coro: Coro) = 60 | ## Starts or continues the execution of coroutine co. The first time you 61 | ## resume a coroutine, it starts running its body. If the coroutine has 62 | ## yielded, resume restarts it. 63 | assert coro != nil 64 | assert coroCur != nil 65 | assert coroCur.status == csRunning 66 | 67 | if coro.status != csSuspended: 68 | let msg = "cannot resume coroutine with status " & $coro.status 69 | echo(msg) 70 | raise newException(CoroException, msg) 71 | 72 | coro.caller = coroCur 73 | coroCur.status = csNormal 74 | 75 | coro.status = csRunning 76 | let coroPrev = coroCur 77 | coroCur = coro 78 | 79 | let frame = getFrameState() 80 | let r = swapcontext(coro.caller.ctx, coro.ctx) # Does not return until coro yields 81 | assert(r == 0) 82 | setFrameState(frame) 83 | 84 | coroCur = coroPrev 85 | if coroCur != nil: 86 | coroCur.status = csRunning 87 | 88 | 89 | proc jield*() = 90 | ## Suspends the execution of the calling coroutine. 91 | let coro = coroCur 92 | assert coro != nil 93 | assert coro.status in {csRunning, csDead} 94 | 95 | if coro.status == csRunning: 96 | coro.status = csSuspended 97 | 98 | let frame = getFrameState() 99 | let r = swapcontext(coro.ctx, coro.caller.ctx) # Does not return until coro resumes 100 | assert(r == 0) 101 | setFrameState(frame) 102 | 103 | 104 | proc running*(): Coro = 105 | ## Return the currently running coro 106 | coroCur 107 | 108 | 109 | coroMain = Coro(status: csRunning) 110 | coroCur = coroMain 111 | 112 | # vi: ft=nim ts=2 sw=2 113 | -------------------------------------------------------------------------------- /eventqueue.nim: -------------------------------------------------------------------------------- 1 | 2 | # Basic poll based event loop 3 | 4 | import posix, tables 5 | 6 | export POLLIN, POLLOUT, POLLERR 7 | 8 | type 9 | 10 | Callback = proc(): bool 11 | 12 | HandlerId = int 13 | 14 | FdHandler = ref object 15 | id: HandlerId 16 | fd: int 17 | events: int 18 | fn: Callback 19 | deleted: bool 20 | 21 | TimerHandler* = ref object 22 | id: HandlerId 23 | interval: float 24 | tWhen: float 25 | fn: Callback 26 | deleted: bool 27 | 28 | Evq* = object 29 | stop: bool 30 | tNow: float 31 | fdHandlers: Table[HandlerId, FdHandler] 32 | timerHandlers: Table[HandlerId, TimerHandler] 33 | nextHandlerId: HandlerId 34 | 35 | 36 | var evq {.threadvar.}: Evq 37 | 38 | proc now(): float = 39 | var ts: Timespec 40 | discard clock_gettime(CLOCK_MONOTONIC, ts) 41 | return ts.tv_sec.float + ts.tv_nsec.float * 1.0e-9 42 | 43 | proc nextId(): HandlerId = 44 | inc evq.nextHandlerId 45 | return evq.nextHandlerId 46 | 47 | # Register/unregister a file descriptor to the loop 48 | 49 | proc addFd*(fd: int, events: int, fn: Callback): HandlerId = 50 | let id = nextId() 51 | evq.fdHandlers[id] = FdHandler(id: id, fd: fd, events: events, fn: fn) 52 | return id 53 | 54 | proc delFd*(id: HandlerId) = 55 | evq.fdHandlers[id].deleted = true 56 | evq.fdHandlers.del id 57 | 58 | # Register/unregister timers 59 | 60 | proc addTimer*(interval: float, fn: Callback): HandlerId = 61 | let id = nextId() 62 | evq.timerHandlers[id] = TimerHandler(id: id, tWhen: now()+interval, interval: interval, fn: fn) 63 | return id 64 | 65 | proc delTimer*(id: HandlerId) = 66 | evq.timerHandlers[id].deleted = true 67 | evq.timerHandlers.del id 68 | 69 | # Run one iteration 70 | 71 | proc poll() = 72 | 73 | if evq.fdHandlers.len == 0: 74 | echo "Nothing in evq" 75 | quit 1 76 | 77 | # Calculate sleep time 78 | 79 | evq.tNow = now() 80 | var tSleep = 100.0 81 | for id, th in evq.timerHandlers: 82 | if not th.deleted: 83 | let dt = th.tWhen - evq.tNow 84 | tSleep = min(tSleep, dt) 85 | 86 | # Collect file descriptors for poll set 87 | 88 | var pfds: seq[TPollfd] 89 | for id, fdh in evq.fdHandlers: 90 | if not fdh.deleted: 91 | pfds.add TPollfd(fd: fdh.fd.cint, events: fdh.events.cshort) 92 | 93 | let r = posix.poll(pfds[0].addr, pfds.len.Tnfds, int(tSleep * 1000.0)) 94 | 95 | # Call expired timer handlers 96 | 97 | evq.tNow = now() 98 | var ths: seq[TimerHandler] 99 | 100 | for id, th in evq.timerHandlers: 101 | if not th.deleted: 102 | if evq.tNow > th.tWhen: 103 | ths.add th 104 | 105 | for th in ths: 106 | if not th.deleted: 107 | let del = th.fn() 108 | if not del: 109 | th.tWhen += th.interval 110 | else: 111 | delTimer(th.id) 112 | 113 | # Call fd handlers with events 114 | 115 | if r == 0: 116 | return 117 | 118 | var fdhs: seq[FdHandler] 119 | 120 | for pfd in pfds: 121 | if pfd.revents != 0: 122 | for id, fdh in evq.fdHandlers: 123 | if not fdh.deleted and fdh.fd == pfd.fd and pfd.revents == fdh.events: 124 | fdhs.add fdh 125 | 126 | for fdh in fdhs: 127 | if not fdh.deleted: 128 | let del = fdh.fn() 129 | if del: 130 | delFd(fdh.id) 131 | 132 | 133 | proc stop*() = 134 | evq.stop = true 135 | 136 | # Run forever 137 | 138 | proc run*() = 139 | while not evq.stop: 140 | poll() 141 | 142 | -------------------------------------------------------------------------------- /io.nim: -------------------------------------------------------------------------------- 1 | 2 | import posix 3 | import coro 4 | import eventqueue 5 | 6 | # This is where the magic happens: This will add a function callback to the 7 | # event queue that will resume the current coro, and the coro will yield itself 8 | # to sleep. It will be awoken when the fd has an event, 9 | 10 | proc waitForFd*(fd: SocketHandle, event: int) = 11 | let co = coro.running() 12 | proc resume_me(): bool = 13 | co.resume() 14 | let fdh = addFd(fd.int, event, resume_me) 15 | jield() 16 | delFd(fdh) 17 | 18 | 19 | # "async" wait for fd and read data 20 | 21 | proc ioRead*(fd: SocketHandle): string = 22 | waitForFd(fd, POLLIN) 23 | var buf = newString(100) 24 | let r = recv(fd, buf[0].addr, buf.len, 0) 25 | buf.setlen(r) 26 | return buf 27 | 28 | # "async" wait for fd and write data 29 | 30 | proc ioWrite*(fd: SocketHandle, buf: string) = 31 | waitForFd(fd, POLLOUT) 32 | discard send(fd, buf[0].unsafeAddr, buf.len, 0) 33 | 34 | 35 | -------------------------------------------------------------------------------- /main.nim: -------------------------------------------------------------------------------- 1 | import coro 2 | import posix 3 | import times 4 | import eventqueue 5 | import io 6 | 7 | let port = 9000 8 | var evq: Evq 9 | 10 | type 11 | MyTask* = ref object of TaskBase 12 | fd: SocketHandle 13 | 14 | # Coroutine handling one client connection. 15 | 16 | proc doClient(task: TaskBase) = 17 | let fd = task.Mytask.fd 18 | ioWrite(fd, "Hello! Please type something.\n") 19 | while true: 20 | let buf = ioRead(fd) 21 | if buf.len > 0: 22 | ioWrite(fd, "You sent " & $buf.len & " characters\n") 23 | else: 24 | echo "Client went away" 25 | break 26 | 27 | # Coroutine handling the server socket 28 | 29 | proc doServer(task: TaskBase) = 30 | let fd = task.MyTask.fd 31 | while true: 32 | waitForFd(fd, POLLIN) 33 | var sa: Sockaddr_in 34 | var saLen: SockLen 35 | let fdc = posix.accept(fd, cast[ptr SockAddr](sa.addr), saLen.addr) 36 | echo "Accepted new client" 37 | newCoro(doClient, MyTask(fd: fdc)) 38 | 39 | # Create TCP server socket and coroutine 40 | 41 | let fd = posix.socket(AF_INET, SOCK_STREAM, 0) 42 | var sa: Sockaddr_in 43 | sa.sin_family = AF_INET.uint16 44 | sa.sin_port = htons(port.uint16) 45 | sa.sin_addr.s_addr = INADDR_ANY 46 | discard bindSocket(fd, cast[ptr SockAddr](sa.addr), sizeof(sa).SockLen) 47 | discard listen(fd, SOMAXCONN) 48 | discard newCoro(doServer, MyTask(fd: fd)) 49 | 50 | echo "TCP server ready on port ", port 51 | 52 | # Just for fun, create a tick tock coroutine 53 | 54 | proc doTick(task: TaskBase) = 55 | var n = 0 56 | while true: 57 | echo "tick ", n 58 | inc n 59 | jield() 60 | 61 | let co = newCoro(doTick) 62 | discard addTimer(1.0, proc(): bool = co.resume()) 63 | 64 | 65 | # Forever run the event loop 66 | run() 67 | 68 | -------------------------------------------------------------------------------- /ucontext.nim: -------------------------------------------------------------------------------- 1 | 2 | type 3 | stack_t* {.importc, header: "".} = object 4 | ss_sp*: pointer 5 | ss_flags*: int 6 | ss_size*: int 7 | 8 | ucontext_t* {.importc, header: "".} = object 9 | uc_link*: ptr ucontext_t 10 | uc_stack*: stack_t 11 | 12 | 13 | proc getcontext*(context: var ucontext_t): int32 {.importc, header: "".} 14 | proc setcontext*(context: var ucontext_t): int32 {.importc, header: "".} 15 | proc swapcontext*(fromCtx, toCtx: var ucontext_t): int32 {.importc, header: "".} 16 | proc makecontext*(context: var ucontext_t, fn: pointer, argc: int32) {.importc, header: "", varargs.} 17 | 18 | 19 | --------------------------------------------------------------------------------