← Back to context

Comment by andyjohnson0

3 days ago

I once worked for about a decade with a body of server-side C code that was written like this. Almost every data structure was either statically allocated at startup or on the stack. I inherited the codebase and kept the original style, once I'd got my head around it.

Positives were that it made the code very easy to reason about, and my impression was that it made it reliable - ownership of data was mostly obvious, and it was hard to (for example) mistakenly use a data structure after it had been free'd. Memory usage under load was very predictable.

Downsides were that data structures (such as string buffers) had to be sized for the max use-case, and code changes had to be hammered into a basically hierarchical data model. It was also hard to incorporate third-party library code - leading to it having its own http and smtp handling, which wasn't great. Some of that might be a consequence of the choice of base language though.

This is a really helpful data point, thanks for sharing it.

What you're describing aligns pretty closely with the behavior I'm trying to achieve—predictable ownership, clear memory lifetimes, and fewer “how did this get freed?” bugs. The downsides you mentioned (like sizing buffers for the worst case, being stuck with a rigid hierarchy, and friction with third-party libraries) are exactly the areas I'm aiming to address.

The difference with what I'm cooking up is: by using lexical scopes with cheap arenas, we can preserve most of that reasoning without the rigid static tree structure. Scopes are flexible and explicit, and you can nest, retry, and promote memory between them without hard-coding everything upfront.

That said, I don't think it completely resolves the ecosystem issues you ran into. If anything, it just makes the boundaries clearer.

If you don't mind me asking, did you run into any specific pain points with refactors that were difficult because of the memory model, or was it more of a cultural constraint?

Also, did this experience influence how you built things afterwards? Where did you land in terms of language/stack?

  • Your comment got my wheels turning so.. quick followup.

    Since you lived with this for such a long stretch, I'd love your gut reaction to the specific escape hatches I'm building in to avoid the rigidity trap:

    1. Arenas grow, not fixed:

    Unlike stack frames, the arenas in my model can expand dynamically. So it's not "size for worst case"—it's "grow as needed, free all at once when scope ends." A request handler that processes 10 items or 10,000 items uses the same code; the arena just grows.

    2. Handles for non-hierarchical references

    When data genuinely needs to outlive its lexical scope or be shared across the hierarchy, you get a generational handle:

        let handle = app_cache.store(expensive_result)
        // handle can be passed around, stored, retrieved later
        // data lives in app scope, not request scope
    

    The handle includes a generation counter, so if the underlying scope dies, dereferencing returns None instead of use-after-free.

    3. Explicit clone for escape:

    If you need to return data from an inner scope to an outer one, you say `clone()` and it copies to the caller's arena. Not automatic, but not forbidden either.

    4. The hierarchy matches server reality:

        App (config, pools, caches)
        └── Worker (thread-local state)
            └── Task (single request)
                └── Frame (loop iteration)
    
    

    For request/response workloads, this isn't an artificial constraint—it's how the work actually flows. The memory model just makes it explicit.

    Where I think it still gets awkward:

    * Graph structures with cycles (need handles, less ergonomic than GC)

    * FFI with libraries expecting malloc/free (planning an `unmanaged` escape hatch)

    * Long-running mutations without periodic scope resets (working on incremental reclamation)

    Do you think this might address the pain you experienced, or am I missing something? Particularly curious whether the handle mechanism would have helped with the cases where you had to hammer code into the hierarchy.

    • There is a lot in what you describe that goes substantially beyond what I had in that codebase. It was basically a set of idioms with some helper code for a few common functions. Having an opinionated, predefined hierarchy is a good approach - there were concepts similar to your app/worker/task in the codebase I dealt with, although the equivalent of worker and task were both (kind of, its been a few years) situated below app.

      In the code I mentioned, a lot of use was made of multi-level arrays of structs, with functions being passed a pointer to a root data structure and one or more array indexes. This made function argument validation somewhat better than just checking for null pointers, as array sizes were mostly stored in their containing struct or were constant. I don't know if that corresponds to your 'handle' concept, but I suspect you're doing something more general-purpose.

      There were simple reader/writer functions for DTOs (which were mostly stored in arrays) but no idea of an ORM.

      Escaping using clone seems sound. The ability to expand scopes seems (if I understand it) powerful, but perhaps makes reasoning about the dynamic behaviour of the code harder. Having some kind of observability around this may help.

      Refactoring wasn't a huge problem. The codebase was basically a statically-linked monolith, so dependencies were simplified. I think that having an explicit way to indicate architecture boundaries might be useful.

      Overall, I suspect that if there are limitations with your approach then it may be that, while it simplifies 80-90% of a problem, the remainder is hard to fit into the architectural framework. Dogfooding some production-level applications should help.

      Good luck. What you're doing is fascinating, and I hope you'll update HN with your progress.