# Xudong-Huang/may: stackful coroutines in Rust, goroutine style

> May gives Rust stackful coroutines with a go! macro, its own network I/O and timers, and a stack per task. It is a different bet from futures-based async, and the stack is the part you have to manage.

**Xudong-Huang/may** — rust stackful coroutine library

- Repository: https://github.com/Xudong-Huang/may
- Stars: 2,437 · Forks: 101
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/xudong-huang-may

## What May is for, and who it is for

May is a Rust library for stackful coroutines. The README describes it as "a high-performance library for programming stackful coroutines" and says it "can be thought as the Rust version of the popular Goroutine". That framing is the whole pitch. If you have written Go, the shape of the code will be familiar: you accept a connection, hand the connection to a task with a macro, and write straight-line blocking code inside that task.

The target audience is engineers writing network servers, proxies, or any program with a large number of concurrent connections where the logic per connection is easier to express sequentially than as a state machine. May ships its own network types under may::net, its own timers, and coroutine-local storage, so a server can be written without pulling in a separate async runtime. The examples directory backs this up: echo.rs, echo_client.rs, echo_udp.rs, http.rs, https.rs and websocket.rs are all server-shaped programs.

It is not a general-purpose replacement for the standard library's threads. The README's caveat section is explicit that the first three rules (no thread-blocking APIs, care with TLS, no long CPU-bound runs) also apply to futures-based systems in Rust. The rule that is specific to May is the fourth: do not exceed the coroutine stack.

## How May schedules coroutines: generator, work stealing, and a stack per task

The mechanism is stackful. The README states the implementation "is based on generator", pointing at Xudong-Huang/generator-rs. That is the key architectural difference from futures-based async: a coroutine owns a real stack, so a function can suspend in the middle of a call chain without the compiler turning it into a state machine. The cost is memory per coroutine and a hard limit you have to size.

Scheduling runs on "a configurable number of threads for multi-core systems", and Cargo.toml shows the default feature set is io_cancel, io_timeout and work_steal. Work stealing is therefore on unless you turn it off. Two other feature flags, rand_work_steal and crossbeam_queue_steal, both depend on work_steal, which suggests the stealing strategy itself is selectable if you need to change it.

Around the scheduler sit the pieces a server needs: efficient asynchronous network I/O, efficient timer management, a semaphore, an MPMC channel, cancellation of coroutines, scoped coroutine creation, and a general select over the coroutine APIs. The README claims all coroutine APIs are compatible with standard library semantics and safe to call in multi-threaded context. Panic handling is per coroutine: a panic "will not affect other coroutines".

The dependency list is worth reading as a map of the design. generator, crossbeam, parking_lot, socket2, num_cpus and core_affinity are all there, plus nix and libc on Unix and windows-sys on Windows. May is not a thin wrapper over an existing reactor; it carries its own I/O layer.

## Installing May and writing a first echo server

May is published on crates.io, and the README badge links to crates.io/crates/may. The package name in Cargo.toml is may, version 0.3.51, edition 2021. Add it the ordinary way:

```toml
[dependencies]
may = "0.3"
```

The README's usage section gives a naive echo server as the first example. It binds a TcpListener from may::net, loops on accept, and spawns a coroutine per connection with the go! macro. Inside the coroutine the read/write loop is blocking in style, which is the point of the library:

```rust
#[macro_use]
extern crate may;

use may::net::TcpListener;
use std::io::{Read, Write};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8000").unwrap();
    while let Ok((mut stream, _)) = listener.accept() {
        go!(move || {
            let mut buf = vec![0; 1024 * 16]; // alloc in heap!
            while let Ok(n) = stream.read(&mut buf) {
                if n == 0 {
                    break;
                }
                stream.write_all(&buf[0..n]).unwrap();
            }
        });
    }
}
```

Run it and it listens on 127.0.0.1:8000; connect with anything that speaks TCP and the bytes come back. Note the comment on the buffer: the README allocates it on the heap, which matters because it lives on the coroutine's stack otherwise. The repository's examples directory holds the fuller versions to read next: examples/echo.rs, examples/echo_client.rs, examples/http.rs, examples/https.rs and examples/websocket.rs. For a single-threaded scheduler, examples/single_thread_schedule.rs is the one to open.

## The caveat list is the real API contract

Most Rust concurrency libraries bury their constraints. May puts them in a document, docs/may_caveat.md, and the README summarises four rules. Three of them are ordinary for cooperative scheduling in Rust: do not call thread-blocking APIs, be careful with thread local storage, and do not run CPU-bound tasks for a long time (the README concedes this is fine "if you don't care about fairness").

The TLS rule deserves a closer look because the README marks a specific pattern unsafe. If a coroutine sets a TLS value, then calls something that yields, then reads that TLS value, the read may see a different coroutine's value. The README's own example is set_tls(), then coroutine::yield_now(), then use_tls(). It is safe if your code does not care about the previous state, or if nothing schedules between the set and the use. That is a subtle failure mode: it will not fail at compile time, and it may not fail in testing.

The fourth rule is the one unique to a stackful design. Every coroutine has a stack with a guard page. Exceeding the stack triggers a segment fault. There is no unwinding, no catchable error, no diagnostic naming the coroutine. The README then points at docs/tune_stack_size.md and says the stack size is what you should really focus on. I would go further: for a stackful library, stack sizing is the operational parameter, not a footnote.

## What May is not: the CPU-bound and TLS-heavy cases

May is the wrong tool in three concrete situations. The first is a workload dominated by long CPU-bound computations. Cooperative scheduling means a coroutine that does not yield holds its thread. The README allows this only if you do not care about fairness, which for a server means one slow request can stall the connections sharing that thread. A work-stealing scheduler does not fix it; the coroutine has to reach a yield point.

The second is code that leans on thread-local storage across suspension points. Because coroutines share OS threads, TLS is not per-task. May offers coroutine-local storage as the alternative, documented in docs/CLS_instead_of_TLS.md, but that means auditing existing code and third-party crates that use thread_local! internally. Any dependency that caches per-thread state and yields in between is a candidate for the unsafe pattern the README describes.

The third is a program that cannot tolerate a segfault as a failure mode. If a coroutine overflows its stack, the process dies. Libraries that return Result on allocation failure give you a place to handle it; a guard page does not. If your service must degrade rather than crash, that is a design constraint May does not meet.

## May versus futures-based async, and versus Go

The nearest alternative is the futures-based stack in Rust, which the README itself references by linking to tokio's echo example. The difference is in the approach, not the feature list. Futures-based async compiles each async fn into a state machine, so there is no per-task stack and the compiler enforces the suspension points. May keeps a real stack and lets you suspend anywhere, which means the compiler cannot check the rules above for you. In exchange, you avoid async fn colouring and the code reads like ordinary blocking code. The README's note is fair: the first three caveats apply to futures-based systems too. The stack is the genuine divergence.

Compared with Go, the README's own analogy, the difference is the runtime. Go's scheduler, garbage collector and stack growth are part of the language runtime; goroutine stacks grow and shrink automatically. May gives you a fixed stack per coroutine and a guard page, and the README sends you to docs/tune_stack_size.md to set it. The programming model is similar; the memory management is not.

For a sense of what the library looks like under load, the README points at may_minihttp and the TechEmpower status page at tfb-status.techempower.com for comparisons. That is the project's own pointer, not an independent benchmark, and the README gives no numbers of its own.

## Maintenance, licensing, and what upgrading costs

The repository is not archived, and the last push was on 2026-08-03. That is recent enough that the project is not abandoned, but the material contains no release notes, so I cannot tell you what changed in any given version or what a migration between versions involves. Cargo.toml puts the crate at 0.3.51, and the 0.x version number is itself information: the API is pre-1.0, and a minor bump can carry breaking changes by convention.

The dependency list is the upgrade cost to watch. generator is pinned at 0.8.9, and the stackful implementation rests on it. nix is at 0.31, windows-sys at 0.61, socket2 at 0.6, parking_lot at 0.12, crossbeam at 0.8. Some of those are low-level crates that track platform APIs, so a May upgrade may pull in a chain of them. The default features (io_cancel, io_timeout, work_steal) are the ones most users will get, and turning work_steal off changes scheduling behaviour, not just binary size.

On licensing: Cargo.toml declares "MIT/Apache-2.0", and the README says May is licensed under either Apache-2.0 or MIT, at your option. Both LICENSE-APACHE and LICENSE-MIT are present at the repository root. Dual licensing at your option is a permissive arrangement, but the choice interacts with your own distribution terms, and the repository's own may_queue subcrate is a separate package with its own version. If your organisation has rules about which of the two you elect, that is a question for your legal team, not for this article.

## Conclusion

Adopt May when you want goroutine-style blocking code in Rust and can accept a stack per coroutine: the go! macro, may::net, CLS and the sync primitives are all in the crate, and the caveat document is the contract you are signing. Do not adopt it if your workload is CPU bound for long stretches, if you depend on thread-local storage that must survive a yield, or if you cannot afford to size stacks and handle the guard-page segfault when one overflows. Before committing, verify the platform list covers your target, read docs/may_caveat.md and docs/tune_stack_size.md, and check whether the io_cancel, io_timeout and work_steal features are the ones you want enabled by default.

## FAQ

### How are coroutines used in Rust with May?

May implements stackful coroutines, so a coroutine owns a real stack and can suspend in the middle of a call chain. You create one with the go! macro, as in the README's echo server, and write ordinary blocking-style code inside it. The README says the implementation is based on generator-rs.

### How do I install May in a Rust project?

May is published on crates.io, and the package name in Cargo.toml is may at version 0.3.51. Add may = "0.3" under [dependencies] in your Cargo.toml. The README's usage section then shows the go! macro and may::net::TcpListener as the starting point.

### Which platforms does May support?

The README lists x86_64 GNU/Linux, x86_64 Windows, x86_64 macOS, AArch64 GNU/Linux and AArch64 macOS. The Cargo.toml has separate dependency sections for Unix (nix, libc) and Windows (windows-sys), which matches that list. Stable, beta and nightly channels are all stated as supported.

### What happens if a May coroutine stack overflows?

Each coroutine stack has a guard page, and the README states that when stack overflow occurs it will trigger a segment fault error. There is no catchable error or unwinding. The README points to docs/tune_stack_size.md for sizing the stack.

### Can I use thread local storage inside a May coroutine?

The README marks one pattern unsafe: set a TLS value, then call a coroutine API that causes scheduling such as coroutine::yield_now(), then read the TLS value. It is safe if your code does not depend on the previous state or if nothing schedules in between. May offers coroutine-local storage as the alternative, described in docs/CLS_instead_of_TLS.md.

## Sources

- [Issues](https://github.com/Xudong-Huang/may/issues)
- [License: Apache-2.0](https://github.com/Xudong-Huang/may/blob/master/LICENSE)
- [README](https://github.com/Xudong-Huang/may/blob/master/README.md)
- [Xudong-Huang/may on GitHub](https://github.com/Xudong-Huang/may)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/xudong-huang-may
