26
Update readme
jonroach 12 Sep 2025 14:35
2 contributors
352 lines12.2 KB
Newer
Older
-
+
commited
{line.log.rev}
on
9 months ago
1
Stackful coroutines in C.
9 months ago
17
2
9 months ago
3
* `Async` coroutines which can pause, waiting for a future.
4
* `Generator` coroutines used as generators for loops.
5
* `Coroutine` the coroutine engine used by `Async` and `Generator`.
9 months ago
17
6
9 months ago
7
Your code doesn't need to do anything special to be a coroutine, and only standard, or commonly available libraries are needed.
9 months ago
17
8
9 months ago
9
## Prerequisites
9 months ago
17
10
9 months ago
11
The goal was to make a system which can be used 'out of the box'. These libraries rely on as much as possible on C's cross-platform comfort zone. C's standard libraries are used as far as possible, but, as `threads.h` is not usually supported, `pthread.h` has been used instead.
9 months ago
17
12
9 months ago
13
You will need to build & link the code as part of your system - `coroutine/async.c`, `coroutine/generator.c` and `coroutine/coroutine.c` - ensure the headers, `include/*`, are available on your include path.
9 months ago
17
14
9 months ago
15
## Quick Start
9 months ago
17
16
9 months ago
17
### Async
9 months ago
17
18
9 months ago
19
To run an Async program:
9 months ago
17
20
9 months ago
21
#include "async.h"
22
main(){
23
Async_StartSystem();
24
void *res = NULL;
25
bool canceled = Async_Run(asyncmain, &param, &res);
26
Async_StopSystem();
27
}
28
29
Async runs tasks, switching between them when the current task waits on an `Async_Future`. `asyncmain()` is run as a task. The start function for any task looks like this:
30
31
bool mytask(void *param, void **res){
32
33
// do your thing here
34
35
return canceled;
36
}
37
38
When `Task` returns from its start function, it returns whether it was canceled. Canceled `Task`s are assumed to have not finished what they were doing.
39
40
Within your async task, create `Async_Task`s and `Async_Task_Await()` them when you want to wait for their result:
41
42
Async_Task task1;
43
Async_Task_ctor(&task1, adifferenttask, &task1param);
44
45
void *result;
46
bool canceled = Async_Task_Await(&task1, &result);
47
48
Async_Task_dtor(&task1);
49
50
// use the result
51
52
When a task needs to wait for something, and wants to allow other tasks to run, it should use a `Future`:
53
54
Async_Future future;
55
Async_Future_ctor(&future);
56
57
// pass the future to the background-thing-which-might-take-a-while
58
59
bool canceled = Async_Future_Await(&future);
60
// get the result, if you want it, from the future
61
62
Async_Future_dtor(&future);
63
64
When the background-thing-which-might-take-a-while has a result:
65
66
Async_Future_SetResult(future, false, result);
67
68
### Generators
69
70
The coroutine system needs to be started, either through `Async_StartSystem()`, or directly with `Coroutine_StartSystem()` if you don't want to do async things.
71
72
You will need a generator function:
73
74
void *yield_my_things(void *param){
75
bool domore = true;
76
77
// loop/call functions to find more values to yield, and when you have one:
78
domore = Generator_Yield(thing);
79
// .. if domore is false, exit your generator - it is being destructed
80
81
// not actually used by generators, but this is a useful convention for bubbling
82
// the flag out to calling functions.
83
return (void *)domore;
84
}
85
86
And to use it:
87
88
Generator gen;
89
Generator_ctor(&gen, yield_my_things, "..");
90
void *thing;
91
while(Generator_Next(&gen, &thing)){
92
// use thing - a value yielded by your generator
93
}
94
Generator_dtor(&gen);
95
96
### Coroutines
97
98
While you can use coroutines directly, it's designed as a system to support more useful patterns, like `Async` and `Generators`.
99
100
The Coroutines system must be started:
101
102
Coroutine_StartSystem();
103
// use coroutines here
104
Coroutine_StopSystem();
105
106
Your coroutine will need to have a start function:
107
108
void *start(void *param){
109
...
110
}
111
112
When there is no coroutine running, start your 'main' coroutine:
113
114
void *result = Coroutine_Run(comain, param);
115
116
Create other coroutines like this:
117
118
Coroutine *cor = Coroutine_New(start);
119
120
When you want a Coroutine to run, or to return from a yield:
121
122
Coroutine_Continue(cor, value, run_early);
123
124
`value` will be start function's parameter, or the value returned from the yield.
125
126
Within the Coroutine, to yield a value:
127
128
void *Coroutine_Yield(value, on_yield, void *me);
129
130
The on_yield function is called after the coroutine has been 'wait'ed, but before the next coroutine is resumed.
131
132
## How it Works
133
134
The coroutine system uses the stack, divided into smaller stacks, for the coroutines. This means you may need to consider whether the coroutine stack size, set by `COROUTINE_STARTUP_STACK_SIZE`, is right for your coroutines, and whether your stack size is enough for the number of coroutines you might run concurrently.
135
136
As each of your thread has its own stack - the coroutine system can be run (or not) independantly on each of your threads. For some special cases, you may want to adjust each of your thread's stack sizes depending on how it is used.
137
138
## Style
139
9 months ago
17
140
The style is influenced by C++. For example, where possible, a `Something *Something_New(a, b, c)` and `Something_Delete(Something *)`, where a `Something` is `malloc`ed, will have corresponding `Somthing_ctor(Somthing *, a, b, c)` and `Something_dtor(Something *)` to initialise and finalise a `Something` on the stack, or within another object. Using `.._ctor()` and `.._dtor()` will be faster as they avoid the `malloc()` and `free()`.
141
142
Something *oneofthem = Something_New();
143
// use oneofthem
144
Something_Delete(oneofthem);
145
146
Can be also be done like this, and this will run faster:
147
148
Something oneofthem;
149
Something_ctor(&oneofthem);
150
// use oneofthem
151
Something_dtor(&oneofthem);
152
9 months ago
153
The exception is `Coroutine_New()` and `Coroutine_Delete()`. The returned `Coroutine` is somewhere on your thread's stack - its memory is managed by the coroutine system, and is allocated and freed quickly.
154
9 months ago
17
155
## Usage
156
157
When you are using coroutines or generators:
158
159
void *myfunc(void *){
160
// your function here
161
}
162
163
Coroutine_StartSystem();
164
Coroutine_Run(myfunc, (void *)myparam);
165
Coroutine_StopSystem();
166
167
If you also use async, then:
168
169
bool myfunc(void *myparam, void **res){
170
// your async function here
171
}
172
173
Async_StartSystem();
174
void *res = NULL;
175
bool canceled = Async_Run(myfunc, myparam, &res);
176
Async_StopSystem();
177
178
While the system is started, you can make many calls to `Coroutine_Run()` or `Async_Run()`. A running system is thread local - each thread you want to use coroutines on will need to be `Coroutine_StartSystem()`ed or `Async_StartSystem()`ed.
179
180
181
9 months ago
182
# API
9 months ago
17
183
9 months ago
184
## Generator
9 months ago
17
185
9 months ago
186
The pattern for a `Generator` is:
187
188
#### A loop which uses the `Generator`
189
9 months ago
17
190
Generator gen;
9 months ago
191
Generator_ctor(&gen, mygen, &param);
9 months ago
17
192
9 months ago
193
void *value;
194
while(Generator_Next(&gen, &value)){
195
// use value here
9 months ago
17
196
}
9 months ago
197
// value is now the return value from the Generator
198
9 months ago
17
199
Generator_dtor(&gen);
200
9 months ago
201
Or:
9 months ago
17
202
9 months ago
203
Generator *gen = Generator_New(mygen, &param);
9 months ago
17
204
9 months ago
205
void *value;
206
while(Generator_Next(gen, &value)){
207
// use value here
208
}
9 months ago
17
209
9 months ago
210
Generator_Delete(gen);
9 months ago
17
211
9 months ago
212
`Generator`s yield a series of `void *`s - what the `void *`s mean is up to you. `Generator_Next()` returns a `bool` to indicate whether the `Generator` has finished.
9 months ago
17
213
9 months ago
214
#### A generator function
9 months ago
17
215
9 months ago
216
void *mygen(void *param){
217
bool domore = true;
218
// The parameter is a pointer to a string of chars
219
for (char *str = param; *str; ++str) {
220
// The value yielded is a pointer to a character in the string
221
domore = Generator_Yield(str);
222
if (!domore){
223
break;
224
}
225
}
226
227
return (void *)domore;
9 months ago
17
228
}
229
9 months ago
230
The `bool` returned from `Generator_Yield()` indicates whether the generator function should yield more values. When it is `false` the `Generator` is being finalised - your generator function should close files, and release any other resources it has claimed, before exiting.
9 months ago
17
231
9 months ago
232
### void Generator_ctor(Generator *gen, void *(*start)(void *), void *param)
9 months ago
17
233
9 months ago
234
Initialise a `Generator`
9 months ago
17
235
9 months ago
236
Generator gen;
237
Generator_ctor(&gen, mystart, &params);
9 months ago
17
238
9 months ago
239
// Generator is used
9 months ago
17
240
9 months ago
241
// ... later:
242
Generator_dtor(&gen);
243
244
### Generator *Generator_New(void *(*)(void *), void *)
245
246
`new` a `Generator` - malloc it and initialise it.
247
248
Generator *gen = Generator_New(mystart, &params);
249
250
// Generator is used
251
252
// ... later:
253
Generator_Delete(gen);
254
255
### void Generator_dtor(Generator *gen)
256
257
Finalise a `Generator`. Once a `Generator` is no longer needed, it must be finalised:
258
259
// earlier...
260
Generator gen;
261
Generator_ctor(&gen, mystart, &params);
262
263
// Generator is used
264
265
// the Generator is no longer needed
266
Generator_dtor(&gen);
267
268
269
### void Generator_Delete(Generator *)
270
271
Finalise then `free()` a `Generator`. Once a `new`ed `Generator` is no longer needed, it must be deleted:
272
273
// earlier...
274
Generator *gen = Generator_New(mystart, &params);
275
276
// Generator is used
277
278
// the Generator is no longer needed
279
Generator_Delete(gen);
280
281
282
### bool Generator_Next(Generator *, void **value)
283
284
Get the next value yielded by the `Generator`.
285
286
void *value;
287
while(Generator_Next(gen, &value)){
288
// use value here
9 months ago
17
289
}
9 months ago
290
9 months ago
291
When `true` is returned, `value` is the value yielded by the `Generator`. When `false` is returned, `value` is the value returned when the `Generator` returned - the `Generator` has finished.
9 months ago
292
9 months ago
293
### bool Generator_Yield(void *)
294
295
Yield a value from a `Generator` function.
296
297
bool domore = Generator_Yield(value);
298
299
`value` is then provided by `Generator_Next()` as the next value from the generator. The `bool` returned by `Generator_Yield()` indicates whether more values should be provided by your generator function. When `true` provide more values if possible. When `false` close files, free memory, free up any other resources and `return`. `false` is returned when the `Generator` is being finalised.
300
9 months ago
301
## Coroutine
302
303
### void Coroutine_StartSystem();
304
305
Start the coroutine system on this thread. When you've finished with `Coroutine` call `Coroutine_Stop()`. `Coroutine` can be started & stopped many times on one thread. The total stack allowed for all coroutines running on a thread is the size of the call stack on that thread.
306
307
### void Coroutine_StopSystem();
308
309
Stop the coroutine system on this thread.
310
311
### Coroutine_Start
312
313
void *(*)(void *param)
314
315
The entry function for a coroutine. The `param` is the value passed to `Coroutine_Continue`, and the `void *` return value can be accessed through the `Coroutine` object.
316
9 months ago
317
### Coroutine *Coroutine_New(Coroutine_Start start)
9 months ago
318
319
Create a new `Coroutine`. The `Coroutine` system must be started to create a `Coroutine`. The stack size available to the coroutine will be `COROUTINE_STACK_SIZE` defined in `coroutine.h`. When you have finished with your `Coroutine`, use `Coroutine_Delete()` to delete it.
320
9 months ago
321
### void Coroutine_Run_Coroutine(Coroutine *cor, void *value)
9 months ago
322
323
Run the `Coroutine` and return when it returns. This is how to start coroutines running in the coroutine system. It is an error for the run coroutine to return before all other coroutines have completed.
324
9 months ago
325
### void *Coroutine_Run(Coroutine_Start start, void *value)
9 months ago
326
327
Convenience wrapper for `Coroutine_Run_Coroutine` which creates the `Coroutine` and retrieves its result.
328
9 months ago
329
### void Coroutine_Delete(Coroutine *cor)
9 months ago
330
331
Use `Coroutine_Delete()` to delete a coroutine when it is no longer needed.
332
9 months ago
333
### void Coroutine_Continue(Coroutine *cor, void *value, bool early)
9 months ago
334
335
Continue the given `Coroutine`. `value` is passed to the coroutine, as `param` to the `start` function, or as the return value from `Coroutine_Yield`. `early` determines whether the continued coroutine is added to the head of tail of the list of runable coroutines.
336
9 months ago
337
### void *Coroutine_Yield(void *value, Coroutine_YieldCallback on_yield, void *this)
9 months ago
338
339
Yield `value` from the current coroutine; this coroutine is moved to the list of coroutines waiting to be continued. The next runable coroutine is run - either by its start routine being called with `value` as its `param`, or by `value`being returned from its `Coroutine_Yield()`.
340
9 months ago
341
### void *Coroutine_GetValue(Coroutine *cor)
9 months ago
342
343
Return the `Coroutine`'s value - the value last yielded, or returned by its `start` routine.
344
9 months ago
345
### Coroutine *Coroutine_GetActive()
9 months ago
346
347
Return whihc coroutine is currently running, ie the caller's `Coroutine`.
348
9 months ago
349
### bool Coroutine_IsRunning(Coroutine *cor)
9 months ago
350
351
Return whether the given coroutine is still running - it may be running, ready to run, or waiting to be continued, but won't have returned from its `start` function.
352