@bugaevc@floss.social avatar

bugaevc

@bugaevc@floss.social

Unix hacker. I do obscure and cursed things.

I hack on Darling, SerenityOS / Ladybird, GNU Hurd / glibc, wl-clipboard, Owl, etc.

I use GNOME, and contribute to freedesktop / GNOME projects sometimes (systemd, PipeWire, GLib, GTK, etc).

I like Rust and dislike Docker.

This profile is from a federated server and may be incomplete. Browse more on the original instance.

bugaevc, to random
@bugaevc@floss.social avatar

Let's look into C++ 20 coroutines 🧵

I assume you're already familiar with general ideas of coroutines, async/await, and CPS / state machine transformation from other languages.

bugaevc,
@bugaevc@floss.social avatar

Let me start by criticizing the C++ 20 design, compared to, say, async/await in Rust:

  1. Contrary to the famous C++ "zero overhead" principle, using coroutines pretty much requires pervasive heap allocations and indirect function calls.
bugaevc,
@bugaevc@floss.social avatar

Even something as simple as breaking out a part of an async function into a sub-function and immediately awaiting its result will result in more heap allocations and more indirect calls; you won't get any inlining.

They say that a sufficiently smart compiler could optimize these things out, but in practice this never happens, not even for trivial functions, not even with -O3.

bugaevc,
@bugaevc@floss.social avatar

This is even more striking considering that Rust async/await has been worked on, in public, since much earlier than 2020. In particular this foundational post from 2016 describes the shortcomings of the approach that's similar to (but still actually better than) the one that C++ 20 ended up taking, and what a much better design looks like: https://aturon.github.io/blog/2016/09/07/futures-design/

bugaevc,
@bugaevc@floss.social avatar
  1. C++ 20 coroutines are just way overdesigned.

People say that the Rust async/await design is complicated (futures, tasks, polling, wakers, pinning, oh my!), but compared to C++ 20 coroutines, Rust design is quite straightforward.

bugaevc,
@bugaevc@floss.social avatar

In theory, all these complications & mental gymnastics are intended to make C++ 20 coroutines very flexible, allowing libraries to provide different styles of coroutine-like abstractions instead of hard-wiring a single design into the core language. In practice, this doesn't seem to buy them much flexibility, compared to what would be possible with a more Rust-like design.

bugaevc,
@bugaevc@floss.social avatar
  1. You cannot just write code with it! For instance you cannot write something like this:

int async_callee() {
co_return 42;
}
int async_caller() {
int a = co_await async_callee();
co_return a + 35;
}

No, you need a bunch of library code/abstractions (that the standard library does not even provide!) to make something like that expressible.

bugaevc,
@bugaevc@floss.social avatar

With that being said, let's look at the actual design.

There are a bunch of explanations of this online, each one, naturally, tries to highlight the aspects that its author deemed most important. I read them so you don't have to; and here's my attempt at explaining it.

bugaevc,
@bugaevc@floss.social avatar

First thing: any function using any of co_return/co_await/co_yield in its body is a coroutine.

A coroutine can suspend its execution, saving its state into a "coroutine frame" (that's almost definitely heap-allocated), to be resumed later. std::coroutine_handle<> is a small wrapper around a pointer to a coroutine frame. It doesn't imply any ownership, can be cheaply copied around, and can be converted to a raw pointer and back using .address()/::from_address().

bugaevc,
@bugaevc@floss.social avatar

Through this handle, you can .resume() a suspended coroutine; to make this work, the coroutine frame includes a vtable, and resuming a coroutine does an indirect call through the vtable. It's important to understand that .resume() is just a regular function call: it's not noreturn or anything like that, and it will, in fact, return, once the coroutine suspends the next time.

bugaevc,
@bugaevc@floss.social avatar

A coroutine suspends by co_await'ing an awaitable value.

Specifically, it calls the .await_suspend(std::coroutine_handle<>) method on it, passing a handle to itself. The method should store the handle somewhere and arrange for something to call .resume() on it later, when the awaited value "is ready". For example, it could arrange for handle.resume() to be called by another thread, or in a later iteration of an event loop.

bugaevc,
@bugaevc@floss.social avatar

Once .await_suspend() returns, the coroutine is suspended, and control returns from the .resume() call that has resumed the coroutine this time (or to the original caller if this is the first time the coroutine suspends; more on this below).

bugaevc,
@bugaevc@floss.social avatar

There are a few knobs to this mechanism: you must also define bool .await_ready() that says whether the coroutine should suspend at all, and .await_suspend() can return a boolean or another coroutine handle to tail-resume in stead of void; also, you can override things to transform the operand of co_await, first into an "awaitable", and then into an "awaiter", and that's what the .await_*() methods would actually get called on (this is somewhat like IntoFuture, if you're keeping track).

bugaevc,
@bugaevc@floss.social avatar

When you eventually .resume() the coroutine, first thing it does is it calls .await_resume() on the awaited value, whose return value is the value the original co_await expression evaluates to (it can also return void or throw an exception).

bugaevc,
@bugaevc@floss.social avatar

Now that we looked at suspending and resuming, let's see how a coroutine gets created in the first place.

You cannot tell from a signature alone whether a function is implemented as a coroutine or not (i.e. whether it has any co_ statements in its body). An external library could flip a function implementation between coroutine / not coroutine without breaking API or ABI. The code to invoke it on the caller side is the same, and you get back an instance of the return type in both cases.

bugaevc, to random
@bugaevc@floss.social avatar

Wait, what?

ekuber, to rust
@ekuber@hachyderm.io avatar

Request for feedback: how would you change this compiler error? Can you tell what's going on? What the problem is? Do you get a sense of how you might be able to solve it?

bugaevc,
@bugaevc@floss.social avatar

@ekuber yes, the "help" part of the error message makes it very clear, great job!

To solve it, I would tweak version requirements (in this case, it's my own crate that brings in one of the two versions, so it should be simple) to get rid of the incompatible versions

bugaevc, to random
@bugaevc@floss.social avatar

New display name, let's see how long this one lasts :D

bugaevc,
@bugaevc@floss.social avatar

@migratory GTK4-B~1.EXE, but otherwise, yes, sure 😀

brooke, to random
@brooke@bikeshed.vibber.net avatar

🎵 parsley sage rosemary and time_t 🎵

bugaevc,
@bugaevc@floss.social avatar

@brooke she pthread_once(+[&] {was a true love of mine})

bugaevc, to random
@bugaevc@floss.social avatar

Apparently, CMake now stores its config.log as YAML...

What was "The C compiler identification is GNU" is now

kind: "message-v1"
backtrace:

  • "/usr/share/cmake/Modules/CMakeDetermineCompilerId.cmake:17 (message)"
  • "/usr/share/cmake/Modules/CMakeDetermineCompilerId.cmake:64 (__determine_compiler_id_test)"
  • "/usr/share/cmake/Modules/CMakeDetermineCCompiler.cmake:123 (CMAKE_DETERMINE_COMPILER_ID)"
    message: |
    The C compiler identification is GNU
drewdevault, to random
@drewdevault@fosstodon.org avatar

I needed a break from Real Work, so I'm speedrunning writing a Unix-ish operating system

Day 3

bugaevc,
@bugaevc@floss.social avatar

@drewdevault wdym about O_DIRECTORY?

bugaevc,
@bugaevc@floss.social avatar

@drewdevault @mxk guess it can be useful — for atomicity, or for reducing the number of file name lookups in find(1)/ftw(3) if readdir returns DT_UNKNOWN — to be able to open() a node first, then fstat it, and if it's a directory do one thing (look up something relative to it with *at()), and do another thing otherwise.

bugaevc, to random
@bugaevc@floss.social avatar

Let's look into virtualization on ARM

🧵

bugaevc,
@bugaevc@floss.social avatar

@stsquad that's... not what I meant.

FEAT_NV2 the feature (which adds the HCR.NV2 bit) improves on the original nested virtualization support. The original nested virtualization support (which did work, but was inefficient) includes two HCR bits, NV and NV1. I'm asking, what's the NV1 bit supposed to be used for — when would I turn it on or off?

bugaevc, to random
@bugaevc@floss.social avatar

.local/bin/fix-wifi

#! /bin/bash
sudo rmmod ath9k_htc && sudo modprobe ath9k_htc

bugaevc,
@bugaevc@floss.social avatar

As of this morning, my Wi-Fi card seemed to have magically fixed itself; it no longer needs the script above.

But then again, it's connected to a Wi-Fi network that no longer exists, somehow working great, beaming data back and forth at full speed.

  • All
  • Subscribed
  • Moderated
  • Favorites
  • megavids
  • mdbf
  • ngwrru68w68
  • modclub
  • magazineikmin
  • thenastyranch
  • rosin
  • khanakhh
  • InstantRegret
  • Youngstown
  • slotface
  • Durango
  • kavyap
  • DreamBathrooms
  • JUstTest
  • GTA5RPClips
  • tacticalgear
  • normalnudes
  • tester
  • osvaldo12
  • everett
  • cubers
  • ethstaker
  • anitta
  • Leos
  • cisconetworking
  • provamag3
  • lostlight
  • All magazines