├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── coru.c ├── coru.h ├── coru_platform.c ├── coru_platform.h ├── coru_util.h └── tests ├── template.fmt ├── test.py ├── test_corners.sh ├── test_nested.sh ├── test_overflow.sh ├── test_parallel.sh ├── test_params.sh └── test_simple.sh /.travis.yml: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | env: 3 | global: 4 | - CFLAGS=-Werror 5 | 6 | # Common test script 7 | script: 8 | # run tests 9 | - make test 10 | 11 | # find code size with smallest configuration 12 | - make clean size 13 | CFLAGS+="-DCORU_NO_ASSERT" 14 | | tee sizes 15 | 16 | # update status if we succeeded, compare with master if possible 17 | - | 18 | if [ "$TRAVIS_TEST_RESULT" -eq 0 ] 19 | then 20 | CURR=$(tail -n1 sizes | awk '{print $1}') 21 | PREV=$(curl -u "$GEKY_BOT_STATUSES" https://api.github.com/repos/$TRAVIS_REPO_SLUG/status/master \ 22 | | jq -re "select(.sha != \"$TRAVIS_COMMIT\") 23 | | .statuses[] | select(.context == \"$STAGE/$NAME\").description 24 | | capture(\"code size is (?[0-9]+)\").size" \ 25 | || echo 0) 26 | STATUS="Passed, code size is ${CURR}B" 27 | if [ "$PREV" -ne 0 ] 28 | then 29 | STATUS="$STATUS ($(python -c "print '%+.2f' % (100*($CURR-$PREV)/$PREV.0)")%)" 30 | fi 31 | fi 32 | 33 | # CI matrix, separate job for each supported platform 34 | jobs: 35 | include: 36 | # x86 64-bit 37 | - stage: test 38 | env: 39 | - STAGE=test 40 | - NAME=coru-x86-64 41 | 42 | # x86 32-bit 43 | - stage: test 44 | env: 45 | - STAGE=test 46 | - NAME=coru-x86-32 47 | - CC="gcc -m32" 48 | install: 49 | - sudo apt-get install gcc-multilib 50 | 51 | # arm thumb 52 | - stage: test 53 | env: 54 | - STAGE=test 55 | - NAME=coru-arm 56 | - CC="arm-linux-gnueabi-gcc --static -mthumb" 57 | - EXEC="qemu-arm" 58 | install: 59 | - sudo apt-get install gcc-arm-linux-gnueabi qemu-user 60 | 61 | # mips 62 | - stage: test 63 | env: 64 | - STAGE=test 65 | - NAME=coru-mips 66 | - CC="mips-linux-gnu-gcc --static" 67 | - EXEC="qemu-mips" 68 | install: 69 | - sudo add-apt-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial main universe" 70 | - sudo apt-get -qq update 71 | - sudo apt-get install gcc-mips-linux-gnu qemu-user 72 | 73 | # Manage statuses 74 | before_install: 75 | - | 76 | curl -u "$GEKY_BOT_STATUSES" -X POST \ 77 | https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ 78 | -d "{ 79 | \"context\": \"$STAGE/$NAME\", 80 | \"state\": \"pending\", 81 | \"description\": \"${STATUS:-In progress}\", 82 | \"target_url\": \"https://travis-ci.org/$TRAVIS_REPO_SLUG/jobs/$TRAVIS_JOB_ID\" 83 | }" 84 | after_failure: 85 | - | 86 | curl -u "$GEKY_BOT_STATUSES" -X POST \ 87 | https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ 88 | -d "{ 89 | \"context\": \"$STAGE/$NAME\", 90 | \"state\": \"failure\", 91 | \"description\": \"${STATUS:-Failed}\", 92 | \"target_url\": \"https://travis-ci.org/$TRAVIS_REPO_SLUG/jobs/$TRAVIS_JOB_ID\" 93 | }" 94 | after_success: 95 | - | 96 | curl -u "$GEKY_BOT_STATUSES" -X POST \ 97 | https://api.github.com/repos/$TRAVIS_REPO_SLUG/statuses/${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT} \ 98 | -d "{ 99 | \"context\": \"$STAGE/$NAME\", 100 | \"state\": \"success\", 101 | \"description\": \"${STATUS:-Passed}\", 102 | \"target_url\": \"https://travis-ci.org/$TRAVIS_REPO_SLUG/jobs/$TRAVIS_JOB_ID\" 103 | }" 104 | # Job control 105 | stages: 106 | - name: test 107 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Christopher Haster 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET = coru.a 2 | ifneq ($(wildcard test.c main.c),) 3 | override TARGET = coru 4 | endif 5 | 6 | CC ?= gcc 7 | AR ?= ar 8 | SIZE ?= size 9 | 10 | SRC += $(wildcard *.c) 11 | OBJ := $(SRC:.c=.o) 12 | DEP := $(SRC:.c=.d) 13 | ASM := $(SRC:.c=.s) 14 | 15 | TEST := $(patsubst tests/%.sh,%,$(wildcard tests/test_*)) 16 | 17 | SHELL = /bin/bash -o pipefail 18 | 19 | ifdef DEBUG 20 | override CFLAGS += -O0 -g3 21 | else 22 | override CFLAGS += -Os 23 | endif 24 | ifdef WORD 25 | override CFLAGS += -m$(WORD) 26 | endif 27 | override CFLAGS += -I. 28 | override CFLAGS += -std=c99 -Wall -pedantic 29 | override CFLAGS += -Wextra -Wshadow -Wjump-misses-init 30 | # Remove missing-field-initializers because of GCC bug 31 | override CFLAGS += -Wno-missing-field-initializers 32 | 33 | 34 | all: $(TARGET) 35 | 36 | asm: $(ASM) 37 | 38 | size: $(OBJ) 39 | $(SIZE) -t $^ 40 | 41 | .SUFFIXES: 42 | test: \ 43 | test_simple \ 44 | test_params \ 45 | test_corners \ 46 | test_overflow \ 47 | test_parallel \ 48 | test_nested 49 | @rm test.c 50 | test_%: tests/test_%.sh 51 | @/bin/bash $< 52 | 53 | -include $(DEP) 54 | 55 | coru: $(OBJ) 56 | $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@ 57 | 58 | %.a: $(OBJ) 59 | $(AR) rcs $@ $^ 60 | 61 | %.o: %.c 62 | $(CC) -c -MMD $(CFLAGS) $< -o $@ 63 | 64 | %.s: %.c 65 | $(CC) -S $(CFLAGS) $< -o $@ 66 | 67 | clean: 68 | rm -f $(TARGET) 69 | rm -f $(OBJ) 70 | rm -f $(DEP) 71 | rm -f $(ASM) 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## coru 2 | 3 | A pocket coroutine library. 4 | 5 | coru was built to solve a common problem for MCU development: Integrating 6 | blocking code with an event-driven system. 7 | 8 | When this happens, MCU developers usually turn to an RTOS. But this is often 9 | like using a sledgehammer to swat a fly. An RTOS introduces complexity, owns 10 | the execution environment, and while an RTOS does provide a rich set of 11 | scheduling features, it comes with an equally large code cost. 12 | 13 | coru provides a much simpler solution with only a 300 B code cost, half the 14 | size of this introduction. 15 | 16 | ### How to use coru 17 | 18 | coru provides delimited coroutines, which are sort of like threads but without 19 | preemption. 20 | 21 | You give coru a function and a stack size and it creates a coroutine: 22 | 23 | ``` c 24 | void func(void *) { 25 | printf("hi!\n"); 26 | } 27 | 28 | 29 | coru_t co; 30 | coru_create(&co, func, NULL, 4096); // returns 0 31 | ``` 32 | 33 | You can start the coroutine by calling coru_resume: 34 | 35 | ``` c 36 | coru_resume(&co); // returns 0, prints hi! 37 | ``` 38 | 39 | In this case, our function will print "hi!" and coru_resume will block until 40 | the function exits. 41 | 42 | But things get really interesting when we call coru_yield in our coroutine: 43 | 44 | ``` c 45 | void func(void *) { 46 | for (int i = 0; i < 10; i++) { 47 | printf("hi %d!\n", i); 48 | coru_yield(); 49 | } 50 | } 51 | 52 | 53 | coru_t co; 54 | coru_create(&co, func, NULL, 4096); // returns 0 55 | ``` 56 | 57 | Now, when we call coru_resume, our function will run until the first 58 | coru_yield: 59 | 60 | ``` c 61 | coru_resume(&co); // returns CORU_ERR_AGAIN, prints hi 0! 62 | ``` 63 | 64 | At this point, our function has been paused to give the main thread a chance to 65 | run. We can resume it with, you guessed it, coru_resume: 66 | 67 | ``` c 68 | coru_resume(&co); // returns CORU_ERR_AGAIN, prints hi 1! 69 | coru_resume(&co); // returns CORU_ERR_AGAIN, prints hi 2! 70 | coru_resume(&co); // returns CORU_ERR_AGAIN, prints hi 3! 71 | ... 72 | coru_resume(&co); // returns CORU_ERR_AGAIN, prints hi 9! 73 | coru_resume(&co); // returns 0 74 | ``` 75 | 76 | coru_resume returns CORU_ERR_AGAIN while the function is running, and returns 0 77 | once the function finishes. We can still call coru_resume, but it will return 0 78 | and do nothing. 79 | 80 | ``` c 81 | coru_resume(&co); // returns 0 82 | coru_resume(&co); // returns 0 83 | coru_resume(&co); // returns 0 84 | ``` 85 | 86 | When you're done with the coroutine, don't forget to clean up its resources 87 | with coru_destroy: 88 | 89 | ``` c 90 | coru_destroy(&co); 91 | ``` 92 | 93 | ### No malloc? No worries 94 | 95 | By default, coru will try to use malloc to create the stack for each coroutine. 96 | You can avoid this by either redefining CORU_MALLOC/CORU_FREE in coru_utils.h 97 | or by calling coru_create_inplace: 98 | 99 | ``` c 100 | coru_t co; 101 | uint8_t co_stack[4096]; 102 | 103 | coru_create_inplace(&co, func, NULL, co_stack, 4096); // returns 0 104 | ``` 105 | 106 | ### What if I overflow my stack? 107 | 108 | coru does provide a simple stack canary, which _usually_ catches stack 109 | overflows. If a stack overflow is detected, coru asserts. At this point the 110 | program can't continue as who knows what memory has been corrupted. 111 | 112 | ``` c 113 | void func(void *) { 114 | func(NULL); 115 | printf("hi!"); 116 | } 117 | 118 | coru_t co; 119 | coru_create(&co, func, NULL, 512); // returns 0 120 | coru_resume(&co); // assertion fails, stack overflow 121 | ``` 122 | 123 | Unfortunately, knowing how much stack to allocate is a hard problem. 124 | 125 | ### Where are the mutexes? 126 | 127 | There's no race conditions here! No preemption has a big benefit in that state 128 | can only change during coru_resume or coru_yield, a granularity that's much 129 | easier for us humans to reason about. Mutate away! 130 | 131 | ``` c 132 | int counter = 0; 133 | 134 | void func(void *) { 135 | while (true) { 136 | // increment the global counter 137 | counter = counter + 1; 138 | printf("%d\n", i); 139 | } 140 | } 141 | 142 | coru_t co1; 143 | coru_create(&co1, func, NULL, 4096); // returns 0 144 | coru_t co2; 145 | coru_create(&co2, func, NULL, 4096); // returns 0 146 | 147 | coru_resume(&co1); // returns CORU_ERR_AGAIN, prints 1 148 | coru_resume(&co2); // returns CORU_ERR_AGAIN, prints 2 149 | coru_resume(&co1); // returns CORU_ERR_AGAIN, prints 3 150 | coru_resume(&co2); // returns CORU_ERR_AGAIN, prints 4 151 | ... 152 | ``` 153 | 154 | Ok, I take that back. Mutate responsibly. Even with coroutines, a large amount 155 | of mutable global state can lead to confusing and unmaintainable programs. 156 | 157 | ### Where's the scheduling? 158 | 159 | So, a part of keeping coru small is that it doesn't have a scheduler. Sometimes 160 | you don't need one or have your own. 161 | 162 | If you do need a scheduler, coru is intended to work well with 163 | [equeue](https://github.com/geky/equeue), its sister event queue library. 164 | 165 | Here's an example of running a coroutine in the background of an event queue 166 | using equeue. If you're not using equeue you should still be able to use this 167 | technique with your own scheduler. 168 | 169 | ``` c 170 | // waiting logic 171 | equeue_t q; 172 | int task_next_wait = 0; 173 | 174 | void task_run(void *p) { 175 | coru_t *co = p; 176 | 177 | next_wait = 0; 178 | int err = coru_resume(co); 179 | if (err == CORU_ERR_AGAIN) { 180 | equeue_call_in(&q, task_next_wait, task_run, co); // returns id 181 | } 182 | } 183 | 184 | void task_wait(int ms) { 185 | task_next_wait = ms; 186 | coru_yield(); 187 | } 188 | 189 | // our task 190 | void func(void *) { 191 | while (true) { 192 | printf("waiting 1000 ms...\n"); 193 | task_wait(1000); // wait 1000 ms 194 | } 195 | } 196 | 197 | // create task and event queue 198 | coru_t co; 199 | coru_create(&co, func, NULL, 4096); // returns 0 200 | 201 | equeue_create(&q, 4096); // returns 0 202 | equeue_call(&q, task_run, &co); // returns id 203 | equeue_dispatch(&q, -1); // runs q, prints "waiting 1000 ms..." every 1000 ms 204 | ``` 205 | 206 | ### But my libraries! 207 | 208 | Right, so a big concern with coroutine systems is how to handle third-party 209 | libraries. The problem is that you can't control when a library calls yield 210 | and need preemption to force libraries to give up the CPU. 211 | 212 | But really, in most cases, the only code that takes any real amount of time 213 | is in drivers, code that waits on hardware. 214 | 215 | Consider [littlefs](https://github.com/geky/littlefs). littlefs must be ported 216 | to a platform's block device, so we already have to write some code. Maybe we 217 | have to poll a flag in our block device. With coru we can turn any polling into 218 | polite yielding. 219 | 220 | ``` c 221 | int spif_read(const struct lfs_config *cfg, lfs_block_t block, 222 | lfs_off_t off, void *buffer, lfs_size_t size) { 223 | uint32_t addr = block*cfg->block_size + off; 224 | uint8_t cmd[4] = { 225 | SPIF_READ, 226 | 0xff & (addr >> 16), 227 | 0xff & (addr >> 8), 228 | 0xff & (addr >> 0) 229 | }; 230 | 231 | // send read command 232 | int err = hal_spi_transfer(cmd, 4, NULL, 0); 233 | if (err) { 234 | return err; 235 | } 236 | 237 | // wait for DMA 238 | while (!hal_spi_isdone()) { 239 | coru_yield(); // yield while polling 240 | } 241 | 242 | // read block 243 | int err = hal_spi_transfer(NULL, 0, buffer, size); 244 | if (err) { 245 | return err; 246 | } 247 | 248 | // wait for DMA 249 | while (!hal_spi_isdone()) { 250 | coru_yield(); // yield while polling 251 | } 252 | } 253 | 254 | // repeat for spif_prog, spif_erase, spif_sync... 255 | ``` 256 | 257 | We can then even wrap up our filesystem operations in coru_resume: 258 | 259 | ``` c 260 | struct asyncfile { 261 | coru_t *co; 262 | int res; 263 | 264 | lfs_t *lfs; 265 | lfs_file_t *file; 266 | void *buffer; 267 | lfs_size_t size; 268 | }; 269 | 270 | void asyncfile_run(void *p) { 271 | // coroutine for non-blocking read 272 | struct asyncfile *af = p; 273 | af->res = lfs_file_read(af->lfs, af->file, af->buffer, af->size); 274 | } 275 | 276 | int asyncfile_open(struct asyncfile *af, 277 | lfs_t *lfs, const char *path, int flags) { 278 | int err = lfs_file_open(lfs, &af->file, path, flags); 279 | if (err) { 280 | return err; 281 | } 282 | 283 | err = coru_create(&af->co, asyncfile_run, af, 4096); 284 | if (err) { 285 | return err; 286 | } 287 | 288 | af->buffer = NULL; 289 | af->size = 0; 290 | return 0; 291 | } 292 | 293 | int asyncfile_read(struct asyncfile *af, 294 | void *buffer, lfs_size_t size) { 295 | // only setup buffer on first read that would block 296 | if (!af->buffer) { 297 | af->res = 0; 298 | af->buffer = buffer; 299 | af->size = size; 300 | } 301 | 302 | // step coroutine, returns CORU_ERR_AGAIN if would block 303 | int err = coru_resume(&af->co); 304 | if (err) { 305 | return err; 306 | } 307 | 308 | // completed a read, reset for next read 309 | af->buffer = NULL; 310 | af->size = 0; 311 | return af->res; 312 | } 313 | 314 | int asyncfile_close(struct asyncfile *af) { 315 | coru_destroy(&af->co); 316 | 317 | int err = lfs_file_close(af->lfs, af->file); 318 | if (err) { 319 | return err; 320 | } 321 | 322 | return 0; 323 | } 324 | ``` 325 | 326 | And hey, now littlefs is non-blocking. That's cool. Sure littlefs may take many 327 | block device operations to read a file, but each operation is a slice where we 328 | give other tasks a chance to run. 329 | 330 | More realistically, you would move the coroutine handling up higher into your 331 | application, with a handful of hardware specific processes that each run in 332 | their own coroutines. 333 | 334 | This is the most common case for MCU libraries. There are a few exceptions, for 335 | example a software crypto library, but for these special cases it's not 336 | unreasonable to manually inject coru_yield calls. 337 | 338 | ``` c 339 | int cmac(uint8_t *output, const uint8_t *input, size_t input_size) { 340 | // create cipher 341 | int err; 342 | mbedtls_cipher_context_t ctx; 343 | mbedtls_cipher_init(ctx); 344 | const mbedtls_cipher_info_t *cipher_info = 345 | mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB); 346 | err = mbedtls_cipher_setup(ctx, cipher_info); 347 | if (err) { 348 | goto cleanup; 349 | } 350 | err = mbedtls_cipher_cmac_starts(ctx, auth_key, AUTH_KEY_SIZE_BITS); 351 | if (err) { 352 | goto cleanup; 353 | } 354 | 355 | // calculate cmac 356 | for (int i = 0; i < input_size; i += chunk_size) { 357 | err = mbedtls_cipher_cmac_update(ctx, &input[i], CMAC_CHUNK_SIZE); 358 | if (err) { 359 | goto cleanup; 360 | } 361 | 362 | coru_yield(); // yield in loop that consumes CPU 363 | } 364 | 365 | // clean up resources 366 | err = mbedtls_cipher_cmac_finish(ctx, output); 367 | if (err) { 368 | goto cleanup; 369 | } 370 | cleanup: 371 | mbedtls_cipher_free(ctx); 372 | return err; 373 | } 374 | ``` 375 | 376 | To help with this, coru_yield outside of a coroutine is simply a noop. 377 | 378 | ### Tests? 379 | 380 | Run make test: 381 | 382 | ``` bash 383 | make test 384 | ``` 385 | 386 | If [QEMU](https://www.qemu.org) supports your processor, you can even 387 | cross-compile these tests: 388 | 389 | ``` bash 390 | make test CC="arm-linux-gnueabi-gcc --static -mthumb" EXEC="qemu-arm" 391 | ``` 392 | 393 | ### What if my processor isn't supported? 394 | 395 | Fret not! The only reason this project exists is to make it easy to port 396 | coroutines to new platforms. I tried to find another coroutine library that 397 | did this, but all of the ones I found required quite a bit of effort to reverse 398 | engineer the porting layer. 399 | 400 | Coroutines _require_ instruction-set specific code in order to manipulate 401 | stacks. This is the main reason coroutines have seen very little use in the MCU 402 | space, where instruction sets can change from project to project. coru is 403 | trying to change that. 404 | 405 | coru requires only two functions: 406 | 407 | ``` c 408 | // Initialize a coroutine stack 409 | // 410 | // This should set up the stack so that two things happen: 411 | // 1. On the first call to coru_plat_yield, the callback cb should be called 412 | // with the data argument. 413 | // 2. After the callback cb returns, the coroutine should then transfer control 414 | // to coru_halt, which does not return. 415 | // 416 | // After coru_plat_init, sp should contain the stack pointer for the new 417 | // coroutine. Also, canary can be set to the end of the stack to enable best 418 | // effort stack checking. Highly suggested. 419 | // 420 | // Any other platform initializations or assertions can be carried out here. 421 | int coru_plat_init(void **sp, uintptr_t **canary, 422 | void (*cb)(void*), void *data, 423 | void *buffer, size_t size); 424 | 425 | // Yield a coroutine 426 | // 427 | // This is where the magic happens. 428 | // 429 | // Several things must happen: 430 | // 1. Store any callee saved registers/state 431 | // 2. Store arg in temporary register 432 | // 3. Swap sp and stack pointer, must store old stack pointer in sp 433 | // 4. Return arg from temporary register 434 | // 435 | // Looking at the i386 implementation may be helpful 436 | uintptr_t coru_plat_yield(void **sp, uintptr_t arg); 437 | ``` 438 | 439 | It may be helpful to look at the other implementation in 440 | [coru_platform.c](coru_platform.c) to see how these can be implemented. 441 | 442 | And if you port coru to a new platform, please create a PR! It would be amazing 443 | to see coru become the biggest collection of MCU stack manipulation functions. 444 | 445 | ### But geky, my processor is a novel quinary vector machine whos only branch instruction is return-if-prime! 446 | 447 | You have more problems than I can help you with. 448 | 449 | ### Related projects 450 | 451 | - [equeue](https://github.com/geky/equeue) - A Swiss Army knife scheduler for 452 | MCUs. equeue is the sister library to coru and provides a simple event-based 453 | scheduler. These two libraries can provide a solid foundation to systems 454 | where a full RTOS may be unnecessary. 455 | 456 | - [Lua coroutines](https://www.lua.org/pil/9.1.html) - coru is based heavily on 457 | Lua's coroutine library, which was a big inspiration for this library and is 458 | one of the best introductions to coroutines in general. 459 | -------------------------------------------------------------------------------- /coru.c: -------------------------------------------------------------------------------- 1 | /* 2 | * coru, a small coroutine library 3 | * 4 | * Copyright (c) 2019 Christopher Haster 5 | * Distributed under the MIT license 6 | */ 7 | #include "coru.h" 8 | #include "coru_platform.h" 9 | 10 | 11 | // Global object for the currently running coroutine 12 | // 13 | // Note that the active coroutine's stack pointer is swapped with 14 | // its parent's while running 15 | static coru_t *coru_active = NULL; 16 | 17 | 18 | // Coroutine operations 19 | int coru_create(coru_t *coru, void (*cb)(void*), void *data, size_t size) { 20 | void *buffer = coru_malloc(size); 21 | if (!buffer) { 22 | return CORU_ERR_NOMEM; 23 | } 24 | 25 | int err = coru_create_inplace(coru, cb, data, buffer, size); 26 | coru->allocated = buffer; 27 | return err; 28 | } 29 | 30 | int coru_create_inplace(coru_t *coru, 31 | void (*cb)(void*), void *data, 32 | void *buffer, size_t size) { 33 | coru->canary = NULL; 34 | coru->allocated = NULL; 35 | 36 | int err = coru_plat_init(&coru->sp, &coru->canary, cb, data, buffer, size); 37 | if (err) { 38 | return err; 39 | } 40 | 41 | if (coru->canary) { 42 | *coru->canary = (uintptr_t)0x636f7275; 43 | } 44 | 45 | return 0; 46 | } 47 | 48 | void coru_destroy(coru_t *coru) { 49 | coru_free(coru->allocated); 50 | } 51 | 52 | int coru_resume(coru_t *coru) { 53 | // we should not be resuming ourselves 54 | CORU_ASSERT(coru != coru_active); 55 | // push previous coroutine's info on the current stack 56 | coru_t *prev = coru_active; 57 | coru_active = coru; 58 | // yield into coroutine 59 | int state = coru_plat_yield(&coru->sp, 0); 60 | // restore previous coroutine's info 61 | coru_active = prev; 62 | return state; 63 | } 64 | 65 | void coru_yield(void) { 66 | // do nothing if we are not a coroutine, this lets yield be used in 67 | // shared libraries 68 | if (!coru_active) { 69 | return; 70 | } 71 | 72 | // check canary, if this fails a stack overflow occured 73 | CORU_ASSERT(!coru_active->canary || 74 | *coru_active->canary == (uintptr_t)0x636f7275); 75 | 76 | // yield out of coroutine 77 | coru_plat_yield(&coru_active->sp, CORU_ERR_AGAIN); 78 | } 79 | 80 | // terminate a coroutine, not public but must be called below 81 | // when a coroutine ends 82 | void coru_halt(void) { 83 | while (true) { 84 | coru_plat_yield(&coru_active->sp, 0); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /coru.h: -------------------------------------------------------------------------------- 1 | /* 2 | * coru, a small coroutine library 3 | * 4 | * Copyright (c) 2019 Christopher Haster 5 | * Distributed under the MIT license 6 | */ 7 | #ifndef CORU_H 8 | #define CORU_H 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | #include "coru_util.h" 15 | 16 | 17 | // Possible error codes, these are negative to allow 18 | // valid positive return values 19 | enum coru_error { 20 | CORU_ERR_OK = 0, // No error 21 | CORU_ERR_AGAIN = -11, // Try again 22 | CORU_ERR_NOMEM = -12, // Out of memory 23 | CORU_ERR_INVAL = -22, // Invalid parameter 24 | }; 25 | 26 | typedef struct coru { 27 | void *sp; // stack information 28 | uintptr_t *canary; // canary location, NULL if not supported 29 | void *allocated; // buffer if allocated, NULL if user provided 30 | } coru_t; 31 | 32 | 33 | // Create a coroutine, dynamically allocating memory for the stack 34 | int coru_create(coru_t *coru, void (*cb)(void*), void *data, size_t size); 35 | 36 | // Create a coroutine using the provided buffer to store the stack 37 | int coru_create_inplace(coru_t *coru, 38 | void (*cb)(void*), void *data, 39 | void *buffer, size_t size); 40 | 41 | // Cleans up any resources dedicated to the coroutine 42 | void coru_destroy(coru_t *coru); 43 | 44 | // Resume a coroutine 45 | // 46 | // This either proceeds from the last call to yield, or starts the coroutine if 47 | // it has not been started yet 48 | // 49 | // Returns CORU_ERR_EAGAIN if the coroutine does not complete during this call. 50 | // If the coroutine completes during this call, or had already completed, 0 is 51 | // returned. 52 | int coru_resume(coru_t *coru); 53 | 54 | // Yields from inside a running coroutine. 55 | // 56 | // Note if not in coroutine this is a noop, this allows yields to be used in 57 | // functions shared between coroutine and non-coroutine code. 58 | void coru_yield(void); 59 | 60 | 61 | #ifdef __cplusplus 62 | } 63 | #endif 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /coru_platform.c: -------------------------------------------------------------------------------- 1 | /* 2 | * coru, platform specific functions 3 | * 4 | * Copyright (c) 2019 Christopher Haster 5 | * Distributed under the MIT license 6 | */ 7 | #include "coru.h" 8 | #include "coru_platform.h" 9 | 10 | 11 | // Terminate a coroutine, defined in coru.c 12 | // 13 | // Must be called when coroutine's main function returns. 14 | extern void coru_halt(void); 15 | 16 | 17 | // Platform specific operations 18 | 19 | // x86 32-bits 20 | #if defined(__i386__) 21 | 22 | // Setup stack 23 | int coru_plat_init(void **psp, uintptr_t **pcanary, 24 | void (*cb)(void*), void *data, 25 | void *buffer, size_t size) { 26 | // check that stack is aligned 27 | CORU_ASSERT((uint32_t)buffer % 4 == 0 && size % 4 == 0); 28 | uint32_t *sp = (uint32_t*)((char*)buffer + size); 29 | 30 | // setup stack 31 | sp[-7] = 0; // edi 32 | sp[-6] = 0; // esi 33 | sp[-5] = 0; // ebx 34 | sp[-4] = 0; // ebp 35 | sp[-3] = (uint32_t)cb; // ret to cb(data) 36 | sp[-2] = (uint32_t)coru_halt; // ret to coru_halt() 37 | sp[-1] = (uint32_t)data; // arg to cb(data) 38 | 39 | // setup stack pointer and canary 40 | *psp = &sp[-7]; 41 | *pcanary = &sp[-size/sizeof(uint32_t)]; 42 | return 0; 43 | } 44 | 45 | // Swap stacks 46 | uintptr_t coru_plat_yield(void **sp, uintptr_t arg); 47 | __asm__ ( 48 | ".globl coru_plat_yield \n" 49 | "coru_plat_yield: \n" 50 | "\t mov 8(%esp), %eax \n" // save arg to eax, return this later 51 | "\t mov 4(%esp), %edx \n" // load new esp to edx 52 | "\t push %ebp \n" // push callee saved registers 53 | "\t push %ebx \n" 54 | "\t push %esi \n" 55 | "\t push %edi \n" 56 | "\t xchg %esp, (%edx) \n" // swap stack 57 | "\t pop %edi \n" // pop callee saved registers 58 | "\t pop %esi \n" 59 | "\t pop %ebx \n" 60 | "\t pop %ebp \n" 61 | "\t ret \n" // return eax 62 | ); 63 | 64 | // x86 64-bits 65 | #elif defined(__amd64__) 66 | 67 | // Here we need a prologue to get data to the callback when 68 | // we startup a coroutine. 69 | void coru_plat_prologue(void); 70 | __asm__ ( 71 | ".globl coru_plat_prologue \n" 72 | "coru_plat_prologue: \n" 73 | "\t mov %r13, %rdi \n" // tail call cb(data) 74 | "\t jmp *%r12 \n" 75 | ); 76 | 77 | // Setup stack 78 | int coru_plat_init(void **psp, uintptr_t **pcanary, 79 | void (*cb)(void*), void *data, 80 | void *buffer, size_t size) { 81 | // check that stack is aligned 82 | CORU_ASSERT((uint64_t)buffer % 4 == 0 && size % 4 == 0); 83 | uint64_t *sp = (uint64_t*)((char*)buffer + size); 84 | 85 | // setup stack 86 | sp[-8] = 0; // rbx 87 | sp[-7] = 0; // rbp 88 | sp[-6] = (uint64_t)cb; // r12 89 | sp[-5] = (uint64_t)data; // r13 90 | sp[-4] = 0; // r14 91 | sp[-3] = 0; // r15 92 | sp[-2] = (uint64_t)coru_plat_prologue; // ret to coru_plat_prologue() 93 | sp[-1] = (uint64_t)coru_halt; // ret to coru_halt() 94 | 95 | // setup stack pointer and canary 96 | *psp = &sp[-8]; 97 | *pcanary = &sp[-size/sizeof(uint64_t)]; 98 | return 0; 99 | } 100 | 101 | // Swap stacks 102 | uintptr_t coru_plat_yield(void **sp, uintptr_t arg); 103 | __asm__ ( 104 | ".globl coru_plat_yield \n" 105 | "coru_plat_yield: \n" 106 | "\t push %r15 \n" // push callee saved registers 107 | "\t push %r14 \n" 108 | "\t push %r13 \n" 109 | "\t push %r12 \n" 110 | "\t push %rbp \n" 111 | "\t push %rbx \n" 112 | "\t xchg %rsp, (%rdi) \n" // swap stack 113 | "\t pop %rbx \n" // pop callee saved registers 114 | "\t pop %rbp \n" 115 | "\t pop %r12 \n" 116 | "\t pop %r13 \n" 117 | "\t pop %r14 \n" 118 | "\t pop %r15 \n" 119 | "\t mov %rsi, %rax \n" // return arg 120 | "\t ret \n" 121 | ); 122 | 123 | // ARM thumb mode 124 | #elif defined(__thumb__) 125 | 126 | // Here we need a prologue to get both data and coru_halt 127 | // into the appropriate registers. 128 | void coru_plat_prologue(void); 129 | __asm__ ( 130 | ".thumb_func \n" 131 | ".global coru_plat_prologue \n" 132 | "coru_plat_prologue: \n" 133 | "\t mov lr, r6 \n" // setup lr to ret to coru_halt() 134 | "\t mov r0, r5 \n" // tail call cb(data) 135 | "\t bx r4 \n" 136 | ); 137 | 138 | // Setup stack 139 | int coru_plat_init(void **psp, uintptr_t **pcanary, 140 | void (*cb)(void*), void *data, 141 | void *buffer, size_t size) { 142 | // check that stack is aligned 143 | CORU_ASSERT((uint32_t)buffer % 4 == 0 && size % 4 == 0); 144 | uint32_t *sp = (uint32_t*)((char*)buffer + size); 145 | 146 | // setup stack 147 | sp[-9] = 0; // r8 148 | sp[-8] = 0; // r9 149 | sp[-7] = 0; // r10 150 | sp[-6] = 0; // r11 151 | sp[-5] = (uint32_t)cb; // r4 152 | sp[-4] = (uint32_t)data; // r5 153 | sp[-3] = (uint32_t)coru_halt; // r6 154 | sp[-2] = 0; // r7 155 | sp[-1] = (uint32_t)coru_plat_prologue; // ret to coru_plat_prologue 156 | 157 | // setup stack pointer and canary 158 | *psp = &sp[-9]; 159 | *pcanary = &sp[-size/sizeof(uint32_t)]; 160 | return 0; 161 | } 162 | 163 | // Swap stacks 164 | uintptr_t coru_plat_yield(void **sp, uintptr_t arg); 165 | __asm__ ( 166 | ".thumb_func \n" 167 | ".global coru_plat_yield \n" 168 | "coru_plat_yield: \n" 169 | "\t push {r4,r5,r6,r7,lr} \n" // push callee saved registers 170 | "\t mov r4, r8 \n" // yes we need these moves, thumb1 can 171 | "\t mov r5, r9 \n" // only push r0-r7 at the same time 172 | "\t mov r6, r10 \n" 173 | "\t mov r7, r11 \n" 174 | "\t push {r4,r5,r6,r7} \n" 175 | "\t mov r2, sp \n" // swap stack, takes several instructions 176 | "\t ldr r3, [r0] \n" // here because thumb1 can't load/store sp 177 | "\t str r2, [r0] \n" 178 | "\t mov sp, r3 \n" 179 | "\t mov r0, r1 \n" // return arg 180 | "\t pop {r4,r5,r6,r7} \n" // pop callee saved registers and return 181 | "\t mov r8, r4 \n" 182 | "\t mov r9, r5 \n" 183 | "\t mov r10, r6 \n" 184 | "\t mov r11, r7 \n" 185 | "\t pop {r4,r5,r6,r7,pc} \n" 186 | ); 187 | 188 | // MIPS 189 | #elif defined(__mips__) 190 | 191 | // Here we need a prologue to get both data and coru_halt 192 | // into the appropriate registers. 193 | void coru_plat_prologue(void); 194 | __asm__ ( 195 | ".globl coru_plat_prologue \n" 196 | "coru_plat_prologue: \n" 197 | "\t move $ra, $s2 \n" // setup $ra to return to core_halt() 198 | "\t addiu $sp, $sp, -4 \n" // tail call cb(data) 199 | "\t move $a0, $s1 \n" 200 | "\t j $s0 \n" 201 | ); 202 | 203 | int coru_plat_init(void **psp, uintptr_t **pcanary, 204 | void (*cb)(void*), void *data, 205 | void *buffer, size_t size) { 206 | // check that stack is aligned 207 | CORU_ASSERT((uint32_t)buffer % 4 == 0 && size % 4 == 0); 208 | uint32_t *sp = (uint32_t*)((char*)buffer + size); 209 | 210 | // setup stack 211 | sp[-10] = (uint32_t)cb; // $s0 212 | sp[-9 ] = (uint32_t)data; // $s1 213 | sp[-8 ] = (uint32_t)coru_halt; // $s2 214 | sp[-7 ] = 0; // $s3 215 | sp[-6 ] = 0; // $s4 216 | sp[-5 ] = 0; // $s5 217 | sp[-4 ] = 0; // $s6 218 | sp[-3 ] = 0; // $s7 219 | sp[-2 ] = 0; // $fp 220 | sp[-1 ] = (uint32_t)coru_plat_prologue; // $ra 221 | 222 | // setup stack pointer and canary 223 | *psp = &sp[-10]; 224 | *pcanary = &sp[-size/sizeof(uint32_t)]; 225 | return 0; 226 | } 227 | 228 | // Swap stacks 229 | uintptr_t coru_plat_yield(void **sp, uintptr_t arg); 230 | __asm__ ( 231 | ".globl coru_plat_yield \n" 232 | "coru_plat_yield: \n" 233 | "\t addiu $sp, $sp, -40 \n" // push callee saved registers 234 | "\t sw $s0, 0($sp) \n" 235 | "\t sw $s1, 4($sp) \n" 236 | "\t sw $s2, 8($sp) \n" 237 | "\t sw $s3, 12($sp) \n" 238 | "\t sw $s4, 16($sp) \n" 239 | "\t sw $s5, 20($sp) \n" 240 | "\t sw $s6, 24($sp) \n" 241 | "\t sw $s7, 28($sp) \n" 242 | "\t sw $fp, 32($sp) \n" 243 | "\t sw $ra, 36($sp) \n" 244 | "\t lw $t0, ($a0) \n" // swap stack 245 | "\t sw $sp, ($a0) \n" 246 | "\t move $sp, $t0 \n" 247 | "\t lw $s0, 0($sp) \n" // pop callee saved registers 248 | "\t lw $s1, 4($sp) \n" 249 | "\t lw $s2, 8($sp) \n" 250 | "\t lw $s3, 12($sp) \n" 251 | "\t lw $s4, 16($sp) \n" 252 | "\t lw $s5, 20($sp) \n" 253 | "\t lw $s6, 24($sp) \n" 254 | "\t lw $s7, 28($sp) \n" 255 | "\t lw $fp, 32($sp) \n" 256 | "\t lw $ra, 36($sp) \n" 257 | "\t addiu $sp, $sp, 40 \n" 258 | "\t move $v0, $a1 \n" // return arg 259 | "\t j $ra \n" 260 | ); 261 | 262 | #else 263 | #error "Unknown platform! Please update coru_platform.c" 264 | #endif 265 | -------------------------------------------------------------------------------- /coru_platform.h: -------------------------------------------------------------------------------- 1 | /* 2 | * coru, platform specific functions 3 | * 4 | * Copyright (c) 2019 Christopher Haster 5 | * Distributed under the MIT license 6 | * 7 | * Each platform needs a small bit of custom code for coroutines because 8 | * of the stack manipulation. Fortunately it only takes two functions. These 9 | * should be added to coru_platform.c. 10 | */ 11 | #ifndef CORU_PLATFORM_H 12 | #define CORU_PLATFORM_H 13 | 14 | #include "coru.h" 15 | 16 | 17 | // Initialize a coroutine stack 18 | // 19 | // This should set up the stack so that two things happen: 20 | // 1. On the first call to coru_plat_yield, the callback cb should be called 21 | // with the data argument. 22 | // 2. After the callback cb returns, the coroutine should then transfer control 23 | // to coru_halt, which does not return. 24 | // 25 | // After coru_plat_init, sp should contain the stack pointer for the new 26 | // coroutine. Also, canary can be set to the end of the stack to enable best 27 | // effort stack checking. Highly suggested. 28 | // 29 | // Any other platform initializations or assertions can be carried out here. 30 | int coru_plat_init(void **sp, uintptr_t **canary, 31 | void (*cb)(void*), void *data, 32 | void *buffer, size_t size); 33 | 34 | // Yield a coroutine 35 | // 36 | // This is where the magic happens. 37 | // 38 | // Several things must happen: 39 | // 1. Store any callee saved registers/state 40 | // 2. Store arg in temporary register 41 | // 3. Swap sp and stack pointer, must store old stack pointer in sp 42 | // 4. Return arg from temporary register 43 | // 44 | // Looking at the i386 implementation may be helpful 45 | uintptr_t coru_plat_yield(void **sp, uintptr_t arg); 46 | 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /coru_util.h: -------------------------------------------------------------------------------- 1 | /* 2 | * coru utilities and config 3 | * 4 | * Copyright (c) 2019 Christopher Haster 5 | * Distributed under the MIT license 6 | * 7 | * Can be overridden by users with their own configuration by defining 8 | * CORU_CONFIG as a header file (-DCORU_CONFIG=coru_config.h) 9 | * 10 | * If CORU_CONFIG is defined, none of the default definitions will be emitted 11 | * and must be provided by the user's config file. To start, I would suggest 12 | * copying coru_util.h and modifying as needed. 13 | */ 14 | #ifndef CORU_UTIL_H 15 | #define CORU_UTIL_H 16 | 17 | #ifdef CORU_CONFIG 18 | #define CORU_STRINGIZE(x) CORU_STRINGIZE2(x) 19 | #define CORU_STRINGIZE2(x) #x 20 | #include CORU_STRINGIZE(CORU_CONFIG) 21 | #else 22 | 23 | // Standard includes, mostly needed for type definitions 24 | #include 25 | #include 26 | #include 27 | #ifndef CORU_NO_MALLOC 28 | #include 29 | #endif 30 | #ifndef CORU_NO_ASSERT 31 | #include 32 | #endif 33 | 34 | #ifdef __cplusplus 35 | extern "C" { 36 | #endif 37 | 38 | 39 | // Runtime assertions 40 | #ifndef CORU_NO_ASSERT 41 | #define CORU_ASSERT(test) assert(test) 42 | #else 43 | #define CORU_ASSERT(test) 44 | #endif 45 | 46 | 47 | // Optional memory allocation 48 | static inline void *coru_malloc(size_t size) { 49 | #ifndef CORU_NO_MALLOC 50 | return malloc(size); 51 | #else 52 | (void)size; 53 | return NULL; 54 | #endif 55 | } 56 | 57 | static inline void coru_free(void *p) { 58 | #ifndef CORU_NO_MALLOC 59 | free(p); 60 | #else 61 | (void)p; 62 | #endif 63 | } 64 | 65 | 66 | #ifdef __cplusplus 67 | } 68 | #endif 69 | 70 | #endif 71 | #endif 72 | -------------------------------------------------------------------------------- /tests/template.fmt: -------------------------------------------------------------------------------- 1 | /// AUTOGENERATED TEST /// 2 | #define _POSIX_C_SOURCE 1 3 | #include "coru.h" 4 | #include 5 | #include 6 | 7 | uintmax_t test_res; 8 | 9 | void test_asserteqm( 10 | const char *file, unsigned line, const char *test, 11 | uintmax_t v, uintmax_t e) {{ 12 | if (v != e) {{ 13 | fprintf(stderr, "\033[31m%s:%u: assert %s failed with %jd, " 14 | "expected %jd\033[0m\n", file, line, test, v, e); 15 | exit(-2); 16 | }} 17 | }} 18 | 19 | #define test_asserteqm(v, e, m) test_asserteqm(__FILE__, __LINE__, m, v, e) 20 | #define test_asserteq(v, e) test_asserteqm(v, e, #v) 21 | #define test_assertm(v, m) test_asserteqm(v, true, m) 22 | #define test_assert(v) test_asserteqm(v, true, #v) 23 | 24 | FILE *test_expect_f; 25 | void test_expect(const char *fmt, ...) {{ 26 | if (!test_expect_f) {{ 27 | return; 28 | }} 29 | 30 | va_list args; 31 | va_start(args, fmt); 32 | vfprintf(test_expect_f, fmt, args); 33 | va_end(args); 34 | }} 35 | 36 | void test(void); 37 | int main() {{ 38 | // create special descriptor for expect traffic 39 | test_expect_f = fdopen(3, "w"); 40 | 41 | // run tests 42 | test(); 43 | }} 44 | 45 | /// test code /// 46 | 47 | {test} 48 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import re 4 | import sys 5 | import subprocess 6 | import os 7 | 8 | def fail(message, info=None): 9 | sys.stdout.write('\033[31m%s\033[0m\n' % message) 10 | if info: 11 | sys.stdout.write('\n%s' % info) 12 | sys.exit(2) 13 | 14 | def generate(test): 15 | with open("tests/template.fmt") as file: 16 | template = file.read() 17 | 18 | lines = [] 19 | for line in re.split('(?<=(?:.;| [{}]))\n', test.read()): 20 | match = re.match('(?: *\n)*( *)(.*)=>(.*);', line, re.DOTALL | re.MULTILINE) 21 | if match: 22 | tab, test, expect = match.groups() 23 | lines.append(tab+'test_res = {test};'.format(test=test.strip())) 24 | lines.append(tab+'test_asserteqm(test_res, {expect}, "{test}");'.format( 25 | test = test.strip().replace('\n', '\\n'), 26 | expect = expect.strip())) 27 | else: 28 | lines.append(line) 29 | 30 | # Create test file 31 | with open('test.c', 'w') as file: 32 | file.write(template.format(test='\n'.join(lines))) 33 | 34 | # Remove build artifacts to force rebuild 35 | try: 36 | os.remove('test.o') 37 | os.remove('lfs') 38 | except OSError: 39 | pass 40 | 41 | def compile(): 42 | err = subprocess.call([ 43 | os.environ.get('MAKE', 'make'), 44 | '--no-print-directory', '-s']) 45 | if err: 46 | fail("Could not compile") 47 | 48 | def execute(expect_error=False): 49 | if 'EXEC' in os.environ: 50 | cmd = [os.environ['EXEC'], "./coru"] 51 | else: 52 | cmd = ["./coru"] 53 | 54 | # create pipe to listen to expect stream (fd 3) 55 | os.pipe() 56 | os.dup2(3, 5) 57 | os.dup2(4, 3) 58 | os.dup2(5, 4) 59 | os.close(5) 60 | expect = os.fdopen(4, "r") 61 | 62 | proc = subprocess.Popen(cmd, 63 | stdout=subprocess.PIPE, 64 | stderr=subprocess.PIPE) 65 | 66 | # yes this is bad, but none of our tests output enough data to 67 | # need to buffer, may need to fix this in the future 68 | proc.wait() 69 | os.close(3) 70 | 71 | stdout = proc.stdout.read() 72 | stderr = proc.stderr.read() 73 | expect = expect.read() 74 | 75 | if (proc.returncode == 0) == expect_error: 76 | fail("Expected %s but test exited with %d" % ( 77 | "error" if expect_error else "no error", proc.returncode), 78 | "stderr:\n%s\n" % stderr) 79 | 80 | if expect and stdout != expect: 81 | fail("Output does not match expected output", 82 | "expected:\n%s\n" % expect + 83 | "actual:\n%s\n" % stdout) 84 | 85 | def main(*args): 86 | test = [a for a in args if not a.startswith('-')] 87 | if test: 88 | with open(test[0]) as file: 89 | generate(file) 90 | else: 91 | generate(sys.stdin) 92 | 93 | compile() 94 | 95 | if '-s' in args: 96 | sys.exit(1) 97 | 98 | execute(expect_error=('-e' in args)) 99 | 100 | if __name__ == "__main__": 101 | sys.exit(main(*sys.argv[1:])) 102 | -------------------------------------------------------------------------------- /tests/test_corners.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | echo "=== Corner case tests ===" 5 | 6 | echo "--- Resume after exit test ---" 7 | tests/test.py << TEST 8 | coru_t coru; 9 | 10 | void count(void *p) { 11 | (void)p; 12 | for (int i = 0; i < 10; i++) { 13 | printf("count: %d\n", i); 14 | coru_yield(); 15 | } 16 | } 17 | 18 | void test(void) { 19 | coru_create(&coru, count, NULL, 8192) => 0; 20 | 21 | for (int i = 0; i < 10; i++) { 22 | coru_resume(&coru) => CORU_ERR_AGAIN; 23 | test_expect("count: %d\n", i); 24 | } 25 | 26 | for (int i = 0; i < 10; i++) { 27 | coru_resume(&coru) => 0; 28 | } 29 | 30 | coru_resume(&coru) => 0; 31 | coru_destroy(&coru); 32 | } 33 | TEST 34 | 35 | echo "--- Yield outside coru test ---" 36 | tests/test.py << TEST 37 | coru_t coru; 38 | 39 | void count(void *p) { 40 | (void)p; 41 | for (int i = 0; i < 10; i++) { 42 | printf("count: %d\n", i); 43 | coru_yield(); 44 | } 45 | } 46 | 47 | void test(void) { 48 | coru_yield(); 49 | 50 | coru_create(&coru, count, NULL, 8192) => 0; 51 | 52 | for (int i = 0; i < 10; i++) { 53 | coru_resume(&coru) => CORU_ERR_AGAIN; 54 | test_expect("count: %d\n", i); 55 | coru_yield(); 56 | } 57 | 58 | coru_resume(&coru) => 0; 59 | coru_destroy(&coru); 60 | 61 | coru_yield(); 62 | } 63 | TEST 64 | 65 | echo "--- Resume self test ---" 66 | tests/test.py -e << TEST 67 | coru_t coru; 68 | 69 | void count(void *p) { 70 | (void)p; 71 | for (int i = 0; i < 10; i++) { 72 | printf("count: %d\n", i); 73 | coru_resume(&coru) => 0; 74 | coru_yield(); 75 | } 76 | } 77 | 78 | void test(void) { 79 | coru_create(&coru, count, NULL, 8192) => 0; 80 | 81 | for (int i = 0; i < 10; i++) { 82 | coru_resume(&coru) => CORU_ERR_AGAIN; 83 | test_expect("count: %d\n", i); 84 | } 85 | 86 | coru_resume(&coru) => 0; 87 | coru_destroy(&coru); 88 | } 89 | TEST 90 | -------------------------------------------------------------------------------- /tests/test_nested.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | echo "=== Nested coru tests ===" 5 | 6 | nested_test() { 7 | echo "--- Nested test, $1 coroutines ---" 8 | tests/test.py << TEST 9 | coru_t coru[$1]; 10 | 11 | void count(void *p) { 12 | int j = (intptr_t)p; 13 | if (j > 0) { 14 | coru_create(&coru[j-1], count, (void*)(intptr_t)(j-1), 8192) => 0; 15 | } 16 | 17 | for (int i = 0; i < 10; i++) { 18 | if (j > 0) { 19 | coru_resume(&coru[j-1]) => CORU_ERR_AGAIN; 20 | test_expect("coru: %d, count: %d\n", j-1, i); 21 | } 22 | 23 | printf("coru: %d, count: %d\n", j, i); 24 | coru_yield(); 25 | } 26 | 27 | if (j > 0) { 28 | coru_resume(&coru[j-1]) => 0; 29 | coru_destroy(&coru[j-1]); 30 | } 31 | } 32 | 33 | void test(void) { 34 | coru_create(&coru[$1-1], count, (void*)(intptr_t)($1-1), 8192) => 0; 35 | 36 | for (int i = 0; i < 10; i++) { 37 | coru_resume(&coru[$1-1]) => CORU_ERR_AGAIN; 38 | test_expect("coru: %d, count: %d\n", $1-1, i); 39 | } 40 | 41 | coru_resume(&coru[$1-1]) => 0; 42 | coru_destroy(&coru[$1-1]); 43 | } 44 | TEST 45 | } 46 | 47 | nested_test 2 48 | nested_test 3 49 | nested_test 10 50 | nested_test 100 51 | -------------------------------------------------------------------------------- /tests/test_overflow.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | echo "=== Stack overflow tests ===" 5 | 6 | echo "--- Stack overflow test ---" 7 | tests/test.py -e << TEST 8 | coru_t coru; 9 | 10 | void recurse(void *p) { 11 | coru_yield(); 12 | recurse(p); 13 | coru_yield(); 14 | } 15 | 16 | void test(void) { 17 | coru_create(&coru, recurse, NULL, 8192) => 0; 18 | 19 | for (int i = 0; i < 1024; i++) { 20 | coru_resume(&coru) => CORU_ERR_AGAIN; 21 | } 22 | } 23 | TEST 24 | -------------------------------------------------------------------------------- /tests/test_parallel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | echo "=== Parallel coru tests ===" 5 | 6 | parallel_test() { 7 | echo "--- Parallel test, $1 coroutines ---" 8 | tests/test.py << TEST 9 | coru_t coru[$1]; 10 | 11 | void count(void *p) { 12 | int j = (intptr_t)p; 13 | for (int i = 0; i < 10; i++) { 14 | printf("coru: %d, count: %d\n", j, i); 15 | coru_yield(); 16 | } 17 | } 18 | 19 | void test(void) { 20 | for (int j = 0; j < $1; j++) { 21 | coru_create(&coru[j], count, (void*)(intptr_t)j, 8192) => 0; 22 | } 23 | 24 | for (int i = 0; i < 10; i++) { 25 | for (int j = 0; j < $1; j++) { 26 | coru_resume(&coru[j]) => CORU_ERR_AGAIN; 27 | test_expect("coru: %d, count: %d\n", j, i); 28 | } 29 | } 30 | 31 | for (int j = 0; j < $1; j++) { 32 | coru_resume(&coru[j]) => 0; 33 | coru_destroy(&coru[j]); 34 | } 35 | } 36 | TEST 37 | } 38 | 39 | parallel_test 2 40 | parallel_test 3 41 | parallel_test 10 42 | parallel_test 100 43 | -------------------------------------------------------------------------------- /tests/test_params.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | echo "=== Parameter tests ===" 5 | 6 | echo "--- Simple create test ---" 7 | tests/test.py << TEST 8 | coru_t coru; 9 | 10 | void count(void *p) { 11 | (void)p; 12 | printf("hey\n"); 13 | coru_yield(); 14 | printf("hi\n"); 15 | coru_yield(); 16 | printf("hello\n"); 17 | coru_yield(); 18 | } 19 | 20 | void test(void) { 21 | coru_create(&coru, count, NULL, 8192) => 0; 22 | coru_resume(&coru) => CORU_ERR_AGAIN; 23 | test_expect("hey\n"); 24 | coru_resume(&coru) => CORU_ERR_AGAIN; 25 | test_expect("hi\n"); 26 | coru_resume(&coru) => CORU_ERR_AGAIN; 27 | test_expect("hello\n"); 28 | coru_resume(&coru) => 0; 29 | coru_destroy(&coru); 30 | } 31 | TEST 32 | 33 | echo "--- Create with arg test ---" 34 | tests/test.py << TEST 35 | coru_t coru; 36 | 37 | void count(void *p) { 38 | int *param = p; 39 | printf("hey %d\n", *param); 40 | coru_yield(); 41 | printf("hi %d\n", *param); 42 | coru_yield(); 43 | printf("hello %d\n", *param); 44 | coru_yield(); 45 | } 46 | 47 | void test(void) { 48 | int param = 0; 49 | coru_create(&coru, count, ¶m, 8192) => 0; 50 | param = 1; 51 | coru_resume(&coru) => CORU_ERR_AGAIN; 52 | test_expect("hey %d\n", param); 53 | param = 2; 54 | coru_resume(&coru) => CORU_ERR_AGAIN; 55 | test_expect("hi %d\n", param); 56 | param = 3; 57 | coru_resume(&coru) => CORU_ERR_AGAIN; 58 | test_expect("hello %d\n", param); 59 | coru_resume(&coru) => 0; 60 | coru_destroy(&coru); 61 | } 62 | TEST 63 | 64 | echo "--- Simple create inplace test ---" 65 | tests/test.py << TEST 66 | coru_t coru; 67 | uint8_t buffer[8192]; 68 | 69 | void count(void *p) { 70 | (void)p; 71 | printf("hey\n"); 72 | coru_yield(); 73 | printf("hi\n"); 74 | coru_yield(); 75 | printf("hello\n"); 76 | coru_yield(); 77 | } 78 | 79 | void test(void) { 80 | coru_create_inplace(&coru, count, NULL, buffer, 8192) => 0; 81 | coru_resume(&coru) => CORU_ERR_AGAIN; 82 | test_expect("hey\n"); 83 | coru_resume(&coru) => CORU_ERR_AGAIN; 84 | test_expect("hi\n"); 85 | coru_resume(&coru) => CORU_ERR_AGAIN; 86 | test_expect("hello\n"); 87 | coru_resume(&coru) => 0; 88 | coru_destroy(&coru); 89 | } 90 | TEST 91 | 92 | echo "--- Create inplace with arg test ---" 93 | tests/test.py << TEST 94 | coru_t coru; 95 | uint8_t buffer[8192]; 96 | 97 | void count(void *p) { 98 | int *param = p; 99 | printf("hey %d\n", *param); 100 | coru_yield(); 101 | printf("hi %d\n", *param); 102 | coru_yield(); 103 | printf("hello %d\n", *param); 104 | coru_yield(); 105 | } 106 | 107 | void test(void) { 108 | int param = 0; 109 | coru_create_inplace(&coru, count, ¶m, buffer, 8192) => 0; 110 | param = 1; 111 | coru_resume(&coru) => CORU_ERR_AGAIN; 112 | test_expect("hey %d\n", param); 113 | param = 2; 114 | coru_resume(&coru) => CORU_ERR_AGAIN; 115 | test_expect("hi %d\n", param); 116 | param = 3; 117 | coru_resume(&coru) => CORU_ERR_AGAIN; 118 | test_expect("hello %d\n", param); 119 | coru_resume(&coru) => 0; 120 | coru_destroy(&coru); 121 | } 122 | TEST 123 | -------------------------------------------------------------------------------- /tests/test_simple.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | echo "=== Simple tests ===" 5 | 6 | echo "--- Single test ---" 7 | tests/test.py << TEST 8 | coru_t coru; 9 | 10 | void count(void *p) { 11 | (void)p; 12 | for (int i = 0; i < 10; i++) { 13 | printf("count: %d\n", i); 14 | coru_yield(); 15 | } 16 | } 17 | 18 | void test(void) { 19 | coru_create(&coru, count, NULL, 8192) => 0; 20 | 21 | for (int i = 0; i < 10; i++) { 22 | coru_resume(&coru) => CORU_ERR_AGAIN; 23 | test_expect("count: %d\n", i); 24 | } 25 | 26 | coru_resume(&coru) => 0; 27 | coru_destroy(&coru); 28 | } 29 | TEST 30 | --------------------------------------------------------------------------------