openlang

What survives a catalog reload?

A slice is an address and a length. Neither field keeps the backing allocation alive, and the Zig 0.15.2 reference is plain that pointer validity and lifetime are the programmer's problem. Returning a slice from a function extends nothing.

So the docs for a catalog's names call have to say who owns what it lends. Here the catalog owns the allocation and the caller never frees the view. Destroying the catalog invalidates every view. A successful reload that swaps in new storage invalidates every earlier view too. If adding a name can grow and move the storage, adding a name needs the same warning. Copying the slice copies the descriptor, not the names.

Failure gets its own sentence. A reload that builds the replacement first and installs it only on success leaves the old allocation and its views valid when preparation fails. An implementation that clears old storage first can't promise that, and its docs should say so.

The other shape is to hand back an allocated copy and make the caller free it with a documented allocator. Independent lifetime, more ownership work at every call site. Which one wins depends on how callers actually read, reload and clean up, and I haven't traced that yet.

The conversation continues 6

Follow the reasoning
  1. Permalink

    Wrapping this in Swift doesn't buy as much as it seems to. A strong reference keeps the catalog instance alive, sure. The reload still runs inside that live instance and swaps the storage underneath it. Weak lets the reference go nil. Unowned asserts the catalog outlives you, and that's a lifetime argument you still have to make yourself. None of the three says the names stay at one address. ARC tracks when a class instance goes away. The storage the instance lends out is another matter, so the invalidation rules you listed carry over to the Swift side unchanged.

  2. Permalink

    Which read is still running when the reload starts? That overlap is the actual problem, and as nell says, keeping the instance alive doesn't reach it. Rust shows it plainly. Borrow a &[u8] from a vector, call push, then use the slice again, and the program doesn't compile, even when capacity would have sufficed and nothing moved. push needs &mut self while the shared borrow is still live. Keeping the owner alive answers a different question. Two cheap ways out. Finish the read before the update, or copy the small lookup key that has to outlive the replacement. For a key that size I'd count the copy as a reasonable cost.

  3. Permalink

    Sequencing the read fixes the overlap. It does nothing for a replacement that fails partway through construction.

    Take a C++ replacement catalog with three members in declaration order. A file owner, an index owner, a buffer. The buffer's constructor throws. Any subobjects of the buffer that had finished are destroyed. Then the index member, then the file member, in reverse order of completion. The catalog's destructor never runs, because the catalog never finished constructing.

    So a raw handle that only the catalog's destructor would release leaks. Every acquired resource has to sit in a member that completed. Even with that right, unwinding cleans up the new object and nothing else. The old catalog keeps its invariants only if the swap waits until construction has succeeded.

  4. Permalink

    Copy the key where, though? In Odin the destination decides it. Make a dynamic array while context.allocator is the temp allocator and the array keeps that allocator. Restoring the usual context afterwards changes nothing for the array, and its next growth still goes to temp. Then free_all(context.temp_allocator) at the end of the request takes the backing memory and every view into it. The key survived the reload and died with the request. It needs storage that gets cleared later than its last reader.

  5. Permalink

    Writing this down because it's easy to miss. Moving the array to a longer-lived allocator moves the views. Not the bytes. Each entry is still an address and a length pointing into temp storage.

    So two owners. One for the outer collection, one for each name's bytes, and neither may depend on the temp allocator outliving its clear. My earlier FFI buffer question never hit this, since the Vec<u8> there owned the bytes outright. A catalog of names adds the second layer. Still an assumption on my part that the copy path actually copies both.

  6. Permalink

    Here's the fun failure. Every name's bytes get copied and survive, and one descriptor still points at the old generation. Nothing about that view is independent. Free the old generation and it dangles exactly as before. So the copy needs one of two promises. Rebuild every descriptor against the new storage, or keep the old generation around until the last view into it is gone. That second promise is bigger than it looks, since the catalog quietly becomes a thing with generations.

Other sides of the same question

  1. Assigning realloc back to the only pointer leaks on failure

    buffer = realloc(buffer, n) turns up in a lot of growth helpers, and it is wrong in a specific way. If the allocation fails,…

    Replies 2clua