Remix.run Logo
atmikemikeb 3 days ago

I thought about dynamically sized types (DSTs) in zig recently. Was considering writing about it. I came to a different conclusion. Why not use zig's opaque?

It's pretty clean at this imo: Less metaprogramming but I think nicer to use in some cases.

  const Connection = opaque {
    pub const Header = struct {
      host_len: usize,
      // add more static fields or dynamic field lengths here
      //buff_len: usize,
    };

    pub fn init(a: std.mem.Allocator, args: struct { host: []const u8 }) *@This() {
      var this = a.allocWithOptions(u8, @sizeOf(Header) + host.len, @alignOf(Header), null);
      @memcpy(this[@sizeOf(Header)..], host);
      return this.ptr; 
    }

    pub fn host(self: *const @This()) []const u8 {
      const bytes: *u8 = @ptrCast(self);
      const header: *Header = @ptrCast(self);
      const data = bytes[@sizeOf(Header)..];
      const host = data[0..header.host_len];
      return host;
    }
  };
going off memory so I expect it to not actually compile, but I've definitely done something like this before.
rvrb 3 days ago | parent | next [-]

I would describe this approach as 'intrusive' - you're storing the lengths of the arrays behind the pointer, enforcing a certain layout of the memory being allocated.

Because the solution outlined in the article stores the lengths alongside the pointer, instead of behind it, there is room for it to work across an ABI (though it currently does not). It's more like a slice in this way.

You could in theory implement your opaque approach using this as a utility to avoid the headache of alignment calculations. For this reason, I think that makes the approach outlined in the article more suitable as a candidate for inclusion in the standard library.

atmikemikeb 3 days ago | parent [-]

Yeah I think mine is more about being able to provide a `host()` helper function instead of a `.get(.host)` meta function. It is somewhat boilerplate-y. I think it's really a matter of taste haha. Likely yours would be useful regardless if this is done a lot, since it abstracts some of it, if one wants that.

rvrb 3 days ago | parent [-]

I've entertained further expanding this API to expose a comptime generated struct of pointers. From the Connection use-case detailed in the article, it would look something like this:

  pub fn getPtrs(self: Self) struct {
      client: *Client,
      host: []u8,
      read_buffer: []u8,
      write_buffer: []u8,
  } {
      return .{
          client: self.get(.client),
          host: self.get(.host),
          read_buffer: self.get(.read_buffer),
          write_buffer: self.get(.write_buffer),
      }
  }
I haven't done this because I'm not yet convinced it's worth the added complexity
ethan_smith 3 days ago | parent | prev [-]

The opaque keyword in Zig doesn't support method definitions - it creates an incomplete type that must be implemented elsewhere, not a type that can have inline methods and fields like your example attempts.

pbaam 3 days ago | parent [-]

https://ziglang.org/documentation/master/#opaque

> opaque {} declares a new type with an unknown (but non-zero) size and alignment. It can contain declarations the same as structs, unions, and enums

So it can contain methods, just like structs. The only thing it cannot contain is fields (which the example above doesn't contain). An opaque type can only be used as a pointer, it's like a `typedef void* ...` in C but with the possibility to add declarations within a namespace

Edit: the documentation doesn't contain any example of an opaque type which happens to have declarations inside. But here is one: https://codeberg.org/andrewrk/daw/src/commit/38d3f0513bf9bfc...