Skip to content

Instantly share code, notes, and snippets.

@aadishv
Created December 26, 2025 04:11
Show Gist options
  • Select an option

  • Save aadishv/3768f6b21fc8524c61f90cd25e0b6090 to your computer and use it in GitHub Desktop.

Select an option

Save aadishv/3768f6b21fc8524c61f90cd25e0b6090 to your computer and use it in GitHub Desktop.
I hate you chroma for making me go on these stupid deep dives

Functions are code that take in inputs and return outputs. Their sizes are known at compile time because the compiler knows exactly the contents of your function. The machine code of the functions are stored in a section (.text) of the binary data. Each function also has a different type. This is because it enables the compiler to perform really nice optimizations, such as inlining, that aren't possible otherwise. Closures are also stored as machine code in the binary data. Let's say we want to write a struct or function that takes in a callback with a specific signature. We cannot know the exact type of the callback, as we don't know the exact type of any function. We thus have a few options for how to type the callback.

We could use impl Fn(...) -> ...; this happens at compile time. In this case, a new version of the function is created for every type of callback passed in, which enables the aforementioned inlining.

We could also use fn(...) -> ... to type for a function pointer to a function with the given signature; such a function pointer will point to a specific part of machine code. When the code runs, then, at the callsite of the callback, the processor will jump to the specified pointer in machine code and run the function. This doesn't allow for inlining, as the callback itself is not known by the compiler, but makes things simpler by avoiding a generic argument.

Note

Feel free to skip the following paragraph. I may be wrong as I barely understand this myself.

However, a big drawback of fn(...) -> ... is that it doesn't allow for side effects or captured variables. If a captured variable is used in a closure, it effectively gets combined with all other captured variables into one struct, which is then added as another argument to the "internal" signature of the closure. However, this means that we can't use fn(...) -> ... because now the "internal" signature of the closure doesn't match the signature we're going to call it with. Thus, if we want the callback input to our function/struct to be able to take in a closure which captures some variables, we can't be sure of the "internal" signature, which means we can't use fn -- a key property of fn is that the signatures of the functions they return to are the same as the signatures that they describe (in other words, fn translates directly to ABI). In order to describe capturing closures, then, where the internal signature is different from the one present to the programmer, we have to put it into new type. This type basically has both the data captured by the closure and a function pointer to the closure's machine code. As noted above, this type is no longer "just a function pointer," so we can't use the fn type, but both impl Fn(...) -> ... and Box<dyn Fn(...) -> ...> work to store such capturing closures. You will generally want to avoid using Box<dyn Fn(...) -> ...> as it entails doing heap allocations. This is Very Bad in vexide as such allocations are relatively expensive for the processor. It does make sense for a few certain scenarios, such as when you want to avoid making users rebuild a library just to change their closure type, or when you want to have a list of capturing closures. Such cases are exceedingly rare, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment