{"id":20997446,"url":"https://github.com/geky/coru","last_synced_at":"2025-05-14T23:30:46.251Z","repository":{"id":53887274,"uuid":"186323656","full_name":"geky/coru","owner":"geky","description":"Pocket coroutine library","archived":false,"fork":false,"pushed_at":"2022-11-13T09:39:02.000Z","size":33,"stargazers_count":79,"open_issues_count":7,"forks_count":11,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-03T00:11:15.385Z","etag":null,"topics":["coroutine","embedded","microcontroller"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/geky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-05-13T01:14:52.000Z","updated_at":"2025-03-29T14:42:54.000Z","dependencies_parsed_at":"2023-01-22T23:46:06.968Z","dependency_job_id":null,"html_url":"https://github.com/geky/coru","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geky%2Fcoru","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geky%2Fcoru/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geky%2Fcoru/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geky%2Fcoru/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/geky","download_url":"https://codeload.github.com/geky/coru/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254247981,"owners_count":22038936,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coroutine","embedded","microcontroller"],"created_at":"2024-11-19T07:39:50.185Z","updated_at":"2025-05-14T23:30:43.547Z","avatar_url":"https://github.com/geky.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"## coru\n\nA pocket coroutine library.\n\ncoru was built to solve a common problem for MCU development: Integrating\nblocking code with an event-driven system.\n\nWhen this happens, MCU developers usually turn to an RTOS. But this is often\nlike using a sledgehammer to swat a fly. An RTOS introduces complexity, owns\nthe execution environment, and while an RTOS does provide a rich set of\nscheduling features, it comes with an equally large code cost.\n\ncoru provides a much simpler solution with only a 300 B code cost, half the\nsize of this introduction.\n\n### How to use coru\n\ncoru provides delimited coroutines, which are sort of like threads but without\npreemption.\n\nYou give coru a function and a stack size and it creates a coroutine:\n\n``` c\nvoid func(void *) {\n    printf(\"hi!\\n\");\n}\n\n\ncoru_t co;\ncoru_create(\u0026co, func, NULL, 4096); // returns 0\n```\n\nYou can start the coroutine by calling coru_resume:\n\n``` c\ncoru_resume(\u0026co); // returns 0, prints hi!\n```\n\nIn this case, our function will print \"hi!\" and coru_resume will block until\nthe function exits.\n\nBut things get really interesting when we call coru_yield in our coroutine:\n\n``` c\nvoid func(void *) {\n    for (int i = 0; i \u003c 10; i++) {\n        printf(\"hi %d!\\n\", i);\n        coru_yield();\n    }\n}\n\n\ncoru_t co;\ncoru_create(\u0026co, func, NULL, 4096); // returns 0\n```\n\nNow, when we call coru_resume, our function will run until the first\ncoru_yield:\n\n``` c\ncoru_resume(\u0026co); // returns CORU_ERR_AGAIN, prints hi 0!\n```\n\nAt this point, our function has been paused to give the main thread a chance to\nrun. We can resume it with, you guessed it, coru_resume:\n\n``` c\ncoru_resume(\u0026co); // returns CORU_ERR_AGAIN, prints hi 1!\ncoru_resume(\u0026co); // returns CORU_ERR_AGAIN, prints hi 2!\ncoru_resume(\u0026co); // returns CORU_ERR_AGAIN, prints hi 3!\n...\ncoru_resume(\u0026co); // returns CORU_ERR_AGAIN, prints hi 9!\ncoru_resume(\u0026co); // returns 0\n```\n\ncoru_resume returns CORU_ERR_AGAIN while the function is running, and returns 0\nonce the function finishes. We can still call coru_resume, but it will return 0\nand do nothing.\n\n``` c\ncoru_resume(\u0026co); // returns 0\ncoru_resume(\u0026co); // returns 0\ncoru_resume(\u0026co); // returns 0\n```\n\nWhen you're done with the coroutine, don't forget to clean up its resources\nwith coru_destroy:\n\n``` c\ncoru_destroy(\u0026co);\n```\n\n### No malloc? No worries\n\nBy default, coru will try to use malloc to create the stack for each coroutine.\nYou can avoid this by either redefining CORU_MALLOC/CORU_FREE in coru_utils.h\nor by calling coru_create_inplace:\n\n``` c\ncoru_t co;\nuint8_t co_stack[4096];\n\ncoru_create_inplace(\u0026co, func, NULL, co_stack, 4096); // returns 0\n```\n\n### What if I overflow my stack?\n\ncoru does provide a simple stack canary, which _usually_ catches stack\noverflows. If a stack overflow is detected, coru asserts. At this point the\nprogram can't continue as who knows what memory has been corrupted.\n\n``` c\nvoid func(void *) {\n    func(NULL);\n    printf(\"hi!\");\n}\n\ncoru_t co;\ncoru_create(\u0026co, func, NULL, 512); // returns 0\ncoru_resume(\u0026co); // assertion fails, stack overflow\n```\n\nUnfortunately, knowing how much stack to allocate is a hard problem.\n\n### Where are the mutexes?\n\nThere's no race conditions here! No preemption has a big benefit in that state\ncan only change during coru_resume or coru_yield, a granularity that's much\neasier for us humans to reason about. Mutate away!\n\n``` c\nint counter = 0;\n\nvoid func(void *) {\n    while (true) {\n        // increment the global counter\n        counter = counter + 1;\n        printf(\"%d\\n\", i);\n    }\n}\n\ncoru_t co1;\ncoru_create(\u0026co1, func, NULL, 4096); // returns 0\ncoru_t co2;\ncoru_create(\u0026co2, func, NULL, 4096); // returns 0\n\ncoru_resume(\u0026co1); // returns CORU_ERR_AGAIN, prints 1\ncoru_resume(\u0026co2); // returns CORU_ERR_AGAIN, prints 2\ncoru_resume(\u0026co1); // returns CORU_ERR_AGAIN, prints 3\ncoru_resume(\u0026co2); // returns CORU_ERR_AGAIN, prints 4\n...\n```\n\nOk, I take that back. Mutate responsibly. Even with coroutines, a large amount\nof mutable global state can lead to confusing and unmaintainable programs.\n\n### Where's the scheduling?\n\nSo, a part of keeping coru small is that it doesn't have a scheduler. Sometimes\nyou don't need one or have your own.\n\nIf you do need a scheduler, coru is intended to work well with\n[equeue](https://github.com/geky/equeue), its sister event queue library.\n\nHere's an example of running a coroutine in the background of an event queue\nusing equeue. If you're not using equeue you should still be able to use this\ntechnique with your own scheduler.\n\n``` c\n// waiting logic\nequeue_t q;\nint task_next_wait = 0;\n\nvoid task_run(void *p) {\n    coru_t *co = p;\n\n    next_wait = 0;\n    int err = coru_resume(co);\n    if (err == CORU_ERR_AGAIN) {\n        equeue_call_in(\u0026q, task_next_wait, task_run, co); // returns id\n    }\n}\n\nvoid task_wait(int ms) {\n    task_next_wait = ms;\n    coru_yield();\n}\n\n// our task\nvoid func(void *) {\n    while (true) {\n        printf(\"waiting 1000 ms...\\n\");\n        task_wait(1000); // wait 1000 ms\n    }\n}\n\n// create task and event queue\ncoru_t co;\ncoru_create(\u0026co, func, NULL, 4096); // returns 0\n\nequeue_create(\u0026q, 4096); // returns 0\nequeue_call(\u0026q, task_run, \u0026co); // returns id\nequeue_dispatch(\u0026q, -1); // runs q, prints \"waiting 1000 ms...\" every 1000 ms\n```\n\n### But my libraries!\n\nRight, so a big concern with coroutine systems is how to handle third-party\nlibraries. The problem is that you can't control when a library calls yield\nand need preemption to force libraries to give up the CPU.\n\nBut really, in most cases, the only code that takes any real amount of time\nis in drivers, code that waits on hardware.\n\nConsider [littlefs](https://github.com/geky/littlefs). littlefs must be ported\nto a platform's block device, so we already have to write some code. Maybe we\nhave to poll a flag in our block device. With coru we can turn any polling into\npolite yielding.\n\n``` c\nint spif_read(const struct lfs_config *cfg, lfs_block_t block,\n        lfs_off_t off, void *buffer, lfs_size_t size) {\n    uint32_t addr = block*cfg-\u003eblock_size + off;\n    uint8_t cmd[4] = {\n        SPIF_READ,\n        0xff \u0026 (addr \u003e\u003e 16),\n        0xff \u0026 (addr \u003e\u003e 8),\n        0xff \u0026 (addr \u003e\u003e 0)\n    };\n\n    // send read command\n    int err = hal_spi_transfer(cmd, 4, NULL, 0);\n    if (err) {\n        return err;\n    }\n\n    // wait for DMA\n    while (!hal_spi_isdone()) {\n        coru_yield(); // yield while polling\n    }\n\n    // read block\n    int err = hal_spi_transfer(NULL, 0, buffer, size);\n    if (err) {\n        return err;\n    }\n\n    // wait for DMA\n    while (!hal_spi_isdone()) {\n        coru_yield(); // yield while polling\n    }\n}\n\n// repeat for spif_prog, spif_erase, spif_sync...\n```\n\nWe can then even wrap up our filesystem operations in coru_resume:\n\n``` c\nstruct asyncfile {\n    coru_t *co;\n    int res;\n\n    lfs_t *lfs;\n    lfs_file_t *file;\n    void *buffer;\n    lfs_size_t size;\n};\n\nvoid asyncfile_run(void *p) {\n    // coroutine for non-blocking read\n    struct asyncfile *af = p;\n    af-\u003eres = lfs_file_read(af-\u003elfs, af-\u003efile, af-\u003ebuffer, af-\u003esize);\n}\n\nint asyncfile_open(struct asyncfile *af,\n        lfs_t *lfs, const char *path, int flags) { \n    int err = lfs_file_open(lfs, \u0026af-\u003efile, path, flags);\n    if (err) {\n        return err;\n    }\n\n    err = coru_create(\u0026af-\u003eco, asyncfile_run, af, 4096);\n    if (err) {\n        return err;\n    }\n\n    af-\u003ebuffer = NULL;\n    af-\u003esize = 0;\n    return 0;\n}\n\nint asyncfile_read(struct asyncfile *af,\n        void *buffer, lfs_size_t size) {\n    // only setup buffer on first read that would block\n    if (!af-\u003ebuffer) {\n        af-\u003eres = 0;\n        af-\u003ebuffer = buffer;\n        af-\u003esize = size;\n    }\n\n    // step coroutine, returns CORU_ERR_AGAIN if would block\n    int err = coru_resume(\u0026af-\u003eco);\n    if (err) {\n        return err;\n    }\n\n    // completed a read, reset for next read\n    af-\u003ebuffer = NULL;\n    af-\u003esize = 0;\n    return af-\u003eres;\n}\n\nint asyncfile_close(struct asyncfile *af) {\n    coru_destroy(\u0026af-\u003eco);\n\n    int err = lfs_file_close(af-\u003elfs, af-\u003efile);\n    if (err) {\n        return err;\n    }\n\n    return 0;\n}\n```\n\nAnd hey, now littlefs is non-blocking. That's cool. Sure littlefs may take many\nblock device operations to read a file, but each operation is a slice where we\ngive other tasks a chance to run.\n\nMore realistically, you would move the coroutine handling up higher into your\napplication, with a handful of hardware specific processes that each run in\ntheir own coroutines.\n\nThis is the most common case for MCU libraries. There are a few exceptions, for\nexample a software crypto library, but for these special cases it's not\nunreasonable to manually inject coru_yield calls.\n\n``` c\nint cmac(uint8_t *output, const uint8_t *input, size_t input_size) {\n    // create cipher\n    int err;\n    mbedtls_cipher_context_t ctx;\n    mbedtls_cipher_init(ctx);\n    const mbedtls_cipher_info_t *cipher_info =\n            mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB);\n    err = mbedtls_cipher_setup(ctx, cipher_info);\n    if (err) {\n        goto cleanup;\n    }\n    err = mbedtls_cipher_cmac_starts(ctx, auth_key, AUTH_KEY_SIZE_BITS);\n    if (err) {\n        goto cleanup;\n    }\n\n    // calculate cmac\n    for (int i = 0; i \u003c input_size; i += chunk_size) {\n        err = mbedtls_cipher_cmac_update(ctx, \u0026input[i], CMAC_CHUNK_SIZE);\n        if (err) {\n            goto cleanup;\n        }\n\n        coru_yield(); // yield in loop that consumes CPU\n    }\n\n    // clean up resources\n    err = mbedtls_cipher_cmac_finish(ctx, output);\n    if (err) {\n        goto cleanup;\n    }\ncleanup:\n    mbedtls_cipher_free(ctx);\n    return err;\n}\n```\n\nTo help with this, coru_yield outside of a coroutine is simply a noop.\n\n### Tests?\n\nRun make test:\n\n``` bash\nmake test\n```\n\nIf [QEMU](https://www.qemu.org) supports your processor, you can even\ncross-compile these tests:\n\n``` bash\nmake test CC=\"arm-linux-gnueabi-gcc --static -mthumb\" EXEC=\"qemu-arm\"\n```\n\n### What if my processor isn't supported?\n\nFret not! The only reason this project exists is to make it easy to port\ncoroutines to new platforms. I tried to find another coroutine library that\ndid this, but all of the ones I found required quite a bit of effort to reverse\nengineer the porting layer.\n\nCoroutines _require_ instruction-set specific code in order to manipulate\nstacks. This is the main reason coroutines have seen very little use in the MCU\nspace, where instruction sets can change from project to project. coru is\ntrying to change that.\n\ncoru requires only two functions:\n\n``` c\n// Initialize a coroutine stack\n//\n// This should set up the stack so that two things happen:\n// 1. On the first call to coru_plat_yield, the callback cb should be called\n//    with the data argument.\n// 2. After the callback cb returns, the coroutine should then transfer control\n//    to coru_halt, which does not return.\n//\n// After coru_plat_init, sp should contain the stack pointer for the new\n// coroutine. Also, canary can be set to the end of the stack to enable best\n// effort stack checking. Highly suggested.\n//\n// Any other platform initializations or assertions can be carried out here.\nint coru_plat_init(void **sp, uintptr_t **canary,\n        void (*cb)(void*), void *data,\n        void *buffer, size_t size);\n\n// Yield a coroutine\n//\n// This is where the magic happens.\n//\n// Several things must happen:\n// 1. Store any callee saved registers/state\n// 2. Store arg in temporary register\n// 3. Swap sp and stack pointer, must store old stack pointer in sp\n// 4. Return arg from temporary register\n//\n// Looking at the i386 implementation may be helpful\nuintptr_t coru_plat_yield(void **sp, uintptr_t arg);\n```\n\nIt may be helpful to look at the other implementation in\n[coru_platform.c](coru_platform.c) to see how these can be implemented.\n\nAnd if you port coru to a new platform, please create a PR! It would be amazing\nto see coru become the biggest collection of MCU stack manipulation functions.\n\n### But geky, my processor is a novel quinary vector machine whos only branch instruction is return-if-prime!\n\nYou have more problems than I can help you with.\n\n### Related projects\n\n- [equeue](https://github.com/geky/equeue) - A Swiss Army knife scheduler for\n  MCUs. equeue is the sister library to coru and provides a simple event-based\n  scheduler. These two libraries can provide a solid foundation to systems\n  where a full RTOS may be unnecessary.\n\n- [Lua coroutines](https://www.lua.org/pil/9.1.html) - coru is based heavily on\n  Lua's coroutine library, which was a big inspiration for this library and is\n  one of the best introductions to coroutines in general.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeky%2Fcoru","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgeky%2Fcoru","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeky%2Fcoru/lists"}