Trying to create a little server application

Hi, I’m very new to Zig, coming from Rust, and basically just finished ziglings after a few weekends. Now I want to create my first little project. It is supposed to be a simple server that manages a list of “work”/“resources” that is distributed to clients. A client requests a resource, does some work, and then confirms it did the work, then the server marks it as done. I am having a bit of trouble with the general setup. My idea was to structure it like this:


const Resource = struct {
    work: []u8,
};

// got this idea from https://matklad.github.io/2025/12/23/zig-newtype-index-pattern.html
const ResourceId = enum(u32) {
    _,
};

/// container for resources
const Pool = struct {
    name: []const u8,

    resources: std.HashMap(todo); // all resources, map of ResourceId->Resource

    // lists of IDs to keep the resource states, trying to be data-oriented or something
    free_resources: std.ArrayList(ResourceId),
    pending_resources: std.ArrayList(ResourceId),
    done_resources: std.ArrayList(ResourceId),

};

The “work” is just a string (maybe json, idk yet) that the client will receive.

  1. Resource is supposed to own the work string/data. Is it right to use a u8 slice for this case? Because my understanding of a slice is that it can point to any part of e.g. a string. How would I be sure that the resource instance owns this? E.g. when I free a Resource instance, I could not safely free the slice, because it could have been created from anything.
  2. For something like a Pool which should be dynamically allocated, is there a convention for naming a “constructor” method (e.g. init)? Is it even normal to have such a method? I think you would have to pass it an allocator, right?
  3. My whole approach feels a bit weird, perhaps because I cannot see the bigger picture due to missing experience.

These are really just the first few lines I wrote and I already have a lot of questions :sweat_smile:.. For me it is definitely harder than Rust, but maybe it is just beginner’s paralysis.. Thank you for any help!!

Hey, welcome to ziggit! (-:

First the easy part:

For something like a Pool which should be dynamically allocated, is there a convention for naming a “constructor” method (e.g. init)? Is it even normal to have such a method? I think you would have to pass it an allocator, right?

Yes, fn init(gpa: Allocator, …) Pool is perfectly fine and the common approach. If you want to allocate & initialize a Pool (which you rarely need in one step), the convention is fn create(gpa: Allocator, …) *Pool

Resource is supposed to own the work string/data. Is it right to use a u8 slice for this case? Because my understanding of a slice is that it can point to any part of e.g. a string. How would I be sure that the resource instance owns this? E.g. when I free a Resource instance, I could not safely free the slice, because it could have been created from anything.

Yes, just like a pointer, a slice can point to anything. Ownership is not enforced by the compiler, but defined by documentation. You can make this clearer by also have init/deinit on the owning struct e.g.

const Resource = struct {
    work: []u8,

    /// Takes ownership of work. Will free it on deinit.
    fn init(work: []const u8) !Resource {
       return .{ .work = work };
    }

    /// gpa must be the Allocator that allocated the original work
    fn deinit(r: *Resource, gpa: Allocator) void {
        gpa.free(r.work);
    }
};

And similar, you can say that all Resources are owned by Pool – also have a pool.deinit function that loops over all resources and calls resource.deinit. Combined with the usual pattern of

var pool : Pool = .init(gpa, …);
defer pool.deinit(gpa);
  1. My whole approach feels a bit weird, perhaps because I cannot see the bigger picture due to missing experience.

Best way is to implement it, see where you struggle, and potentially redesign. (-: Keep it simple first, and then see where it could be optimized. When you implement it, you’ll may find these questions: when a work is done, it needs to be put from pending_resources to done_resources. With an ArrayList it’s O(n) to find the index in the array. Do you even need to iterate over pending_resources? And when you already have a free list of free_resources, do you need a HashMap? Instead the ResourceID could simply be indices into an array.

4 Likes

your api can duplicate the string to ensure it is owned, you can also just require the caller pass you a whole, allocated slice.

Or you can leave deallocation to the caller.

with create that @xash mentioned, that is the convention for functions that return a new, heap allocated, instance of the type.
You likely don’t need that, in fact you likely don’t need any allocation during initialisation.

In fact, with the code you shared, you are close to not needing an init function at all.

2 Likes

Thank you both, that is very very helpful !

I also thought about using the Id as an index, but then realized that I want to persist the work that is done. I.e. I have a file defining all the work for one pool, with one work per line. When a work is done, the ID is saved to a “done” file. That would not be safe with index IDs because the original spec might change when the server restarts. So I think using the hash of the work would be good as an ID that can be saved, too.

At this point maybe it would be better to use something like sqlite, but that’s kinda too complicated for me at the moment ^^.

Regarding allocation, I saw that ArrayList is derived from Aligned and Aligned is also given an allocator for most methods that need to manage its memory. Above it is AlignedManaged with a note about it being deprecated. AlignedManaged keeps a reference to the allocator instead. So I was wondering if/why this is discouraged? @xash also put the allocator as an argument, instead of e.g. saving a reference to it, and I see this with a lot of std stuff as well. I would have thought that when the struct holds a reference to the allocator, it would be a bit safer when calling deinit because you don’t need to have the original allocator. But maybe that is not an issue in practice?

I never had to think so much about the heap. I do C a lot too, but there it is just malloc/calloc mostly, and in Rust it’s just Box::new when you want to have a heap allocation.

You can still operate with indexes for runtime id’s and have the hash be a persistent id that gets mapped to a runtime id when necessary.

The persistent id could also be an index, though a hash is reproducible by the client without interacting with the server.

For one thing, it makes the type bigger, which means that managed containers compose worse. I believe I’ve heard that it also makes it harder for LLVM to devirtualize the allocator calls, although I don’t know really.

I think finally, just based on vibes, it’s great when you know that a function potentially allocates because of its signature, and that conversely you know that it cannot allocate again because of its signature.

2 Likes

Passed in to specific functions shows clearer where allocations occur, and encourages better allocation practices, e.g allocate up front.

Storing it is also inefficient of you have multiple, and often not necessary, usually you know at programming time what allocator to use and rarely would have more than 2 to choose from.

In the cases where you do need to store the allocator, it is just better for you to do so in your own type, as often it is used for more than a single collection.

when performance matters, or lifetimes are difficult/problematic, C, and to a lesser degree rust, programmers would want exactly the control zig provides here. In fact, custom allocators are not uncommon in C, most allocation heavy libraries/programs will have at least an arena of some kind.

2 Likes

Sounds to me like it would make most sense for the server to respond with the ResourceId / ExternalId (which is managed by the server) of the work item. Then you only need to make sure that the server doesn’t hand out two identical ones at the same time, otherwise the clients can treat them as opaque handles.

How do you get the work data, is that part of what the server sends or do you load it from somewhere else using the ResourceId or meta data you got from the server?

It’s not difficult at all. There are several SQLite integration implementations for Zig. I am using vrischmann/zig-sqlite and I am very satisfied.

1 Like

Can you explain what you mean by “allocate up front”?

The clients need the actual information in the “work” field to do their work. So the clients make a request, then get a resource that is not yet completed, i.e. from the free_resources list of the pool (that was missing from earlier), along with its id. Then later they confirm that they did the work for the id (on a trust-me-bro basis)

Thank you, I’ll check it out! For now I will try to roll my own using the standard library only, to learn about it

list.ensureUnusedCapacity(gpa, 10)
for (0..10) |i| list.appendAssumeCapacity(i);

When you know space you need, or can calculate it, pre allocate it so that it is a single allocation, instead of multiple. It is faster period, and isolates failure to a single call instead of spread across multiple.

2 Likes

Thanks, that makes sense. I am starting to get a bit better feel for this and managed to at least read some files for now ^^ I have another question (if it’s not too much to ask)
With std.HashMap there is the put method, which in turn calls some putContext

pub fn putContext(self: *Self, allocator: Allocator, key: K, value: V, ctx: Context) Allocator.Error!void {
    const result = try self.getOrPutContext(allocator, key, ctx);
    result.value_ptr.* = value;
}

I see that the value: V parameter gets passed in by value (copied?), to then be copied again into the location pointed to by value_ptr. Why is the parameter not passed by reference?

there used to be parameter reference optimisation, where zig would turn parameters into pointers for you if they were large enough.

But it was removed as it could cause illegal/unintended behaviour.

The optimiser could and likely will make that optimisation anyway as parameters are const.

The hash map has no idea what is faster, not to mention those inner functions are public so can be called by you, a copy is semantically correct.

1 Like

I managed to stitch something together. It is somewhat of an abomination, lots of TODOs and missing deinitializations ^^ but it works! I think I did some things OK and was lazy at some other parts. It was a good first project and I will definitely pursue zig further, maybe in an embedded project that I have in my queue.

I have to say that the standard library of zig is outstanding. I was surprised how readable it is, especially with my limited knowledge of the language. There is often little documentation but details can be inferred from the source code very easily. When you try to read the standard library of Rust, it’s much more complicated with a lot of indirection.

1 Like