summary refs log tree commit diff stats
path: root/lib/pure/coro.nim
blob: 6ef5f6f54bfc126633c8e856d719623a3fc99045 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#
#
#            Nim's Runtime Library
#        (c) Copyright 2015 Rokas Kupstys
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

when not defined(nimCoroutines):
  {.error: "Coroutines require -d:nimCoroutines".}

import os, times
import macros
import arch
import lists

const coroDefaultStackSize = 512 * 1024


type Coroutine = ref object
  # prev: ptr Coroutine
  # next: ptr Coroutine
  ctx: JmpBuf
  fn: proc()
  started: bool
  lastRun: float
  sleepTime: float
  stack: pointer
  stacksize: int

var coroutines = initDoublyLinkedList[Coroutine]()
var current: Coroutine
var mainCtx: JmpBuf


proc GC_addStack(starts: pointer) {.cdecl, importc.}
proc GC_removeStack(starts: pointer) {.cdecl, importc.}
proc GC_setCurrentStack(starts, pos: pointer) {.cdecl, importc.}


proc coroStart*(c: proc(), stacksize: int=coroDefaultStackSize) =
  ## Adds coroutine to event loop. It does not run immediately.
  var coro = Coroutine()
  coro.fn = c
  while coro.stack == nil:
    coro.stack = alloc0(stacksize)
  coro.stacksize = stacksize
  coroutines.append(coro)

{.push stackTrace: off.}
proc coroYield*(sleepTime: float=0) =
  ## Stops coroutine execution and resumes no sooner than after ``sleeptime`` seconds.
  ## Until then other coroutines are executed.
  var oldFrame = getFrame()
  var sp {.volatile.}: pointer
  GC_setCurrentStack(current.stack, cast[pointer](addr sp))
  current.sleepTime = sleep_time
  current.lastRun = epochTime()
  if setjmp(current.ctx) == 0:
    longjmp(mainCtx, 1)
  setFrame(oldFrame)
{.pop.}

proc coroRun*() =
  ## Starts main event loop which exits when all coroutines exit. Calling this proc
  ## starts execution of first coroutine.
  var node = coroutines.head
  var minDelay: float = 0
  var frame: PFrame
  while node != nil:
    var coro = node.value
    current = coro
    os.sleep(int(minDelay * 1000))

    var remaining = coro.sleepTime - (epochTime() - coro.lastRun);
    if remaining <= 0:
      remaining = 0
      let res = setjmp(mainCtx)
      if res == 0:
        frame = getFrame()
        if coro.started:            # coroutine resumes
          longjmp(coro.ctx, 1)
        else:
          coro.started = true       # coroutine starts
          var stackEnd = cast[pointer](cast[ByteAddress](coro.stack) + coro.stacksize)
          GC_addStack(coro.stack)
          coroSwitchStack(stackEnd)
          coro.fn()
          coroRestoreStack()
          GC_removeStack(coro.stack)
          var next = node.prev
          coroutines.remove(node)
          dealloc(coro.stack)
          node = next
          setFrame(frame)
      else:
        setFrame(frame)

    elif remaining > 0:
      if minDelay > 0 and remaining > 0:
        minDelay = min(remaining, minDelay)
      else:
        minDelay = remaining

    if node == nil or node.next == nil:
      node = coroutines.head
    else:
      node = node.next


proc coroAlive*(c: proc()): bool =
  ## Returns ``true`` if coroutine has not returned, ``false`` otherwise.
  for coro in items(coroutines):
    if coro.fn == c:
      return true

proc coroWait*(c: proc(), interval=0.01) =
  ## Returns only after coroutine ``c`` has returned. ``interval`` is time in seconds how often.
  while coroAlive(c):
    coroYield interval


when isMainModule:
  var stackCheckValue = 1100220033
  proc c2()

  proc c1() =
    for i in 0 .. 3:
      echo "c1"
      coroYield 0.05
    echo "c1 exits"


  proc c2() =
    for i in 0 .. 3:
      echo "c2"
      coroYield 0.025
    coroWait(c1)
    echo "c2 exits"

  coroStart(c1)
  coroStart(c2)
  coroRun()
  echo "done ", stackCheckValue