Efficiently create/concat strings or binary data, optionally using a thread-safe pool with pre-allocated static buffers.

Applications that need to create many StringBuilders should consider using a zul.StringBuilder.Pool. StringBuilders created via the pool have a re-usable static buffer which can eliminate or reduce the need for dynamic memory allocations (assume the size of the static buffer is larger than the typical data written).

// StringBuilder can be used to efficiently concatenate strings
// But it can also be used to craft binary payloads.
var sb = zul.StringBuilder.init(allocator);
defer sb.deinit();

// We're going to generate a 4-byte length-prefixed message.
// We don't know the length yet, so we'll skip 4 bytes
// We get back a "view" which will let us backfill the length
var view = try sb.skip(4);

// Writes a single byte
try sb.writeByte(10);

// Writes a []const u8
try sb.write("hello");

// Using our view, which points to where the view was taken,
// fill in the length.
view.writeU32Big(@intCast(sb.len() - 4));

std.debug.print("{any}\n", .{sb.string()});
// []u8{0, 0, 0, 6, 10, 'h', 'e', 'l', 'l', 'o'}

zul.StringBuilder is a wrapper around a dynamically growable []u8 and is designed for string concatenation or creating binary payloads. While std.ArrayList(u8) can be used for the same purpose, the non-generic nature of StringBuilder results in a cleaner interface.

endian: std.builtin.Endian

The endianness to use, .big or .little, when encoding integers. Defaults to the current architecture.

Creates a StringBuilder.

Releases all allocated memory. The StringBuilder should not be used after this is called.

Resets the StringBuilder's length to 0 without freeing the underlying buffer. It is safe to re-use the StringBuilder.

Creates a copy of the written data using the provided allocator.

Ensures that the the underlying buffer has n spare bytes. If not, tries to allocates enough memory to allocate n additional bytes.

This can be used to pre-grow the buffer prior to making a large number of writes.

fn fromOwnedSlice(
	// When deinit is called on the returned StringBuilder, buf will be freed
	// with this allocator. Thus, buf must have been created with this
	// allocator.
	allocator: std.mem.Allocator,

	// The position of the returned StringBuilder will be set to buf.len (thus
	// any subsequent writes will be appended)
	buf: []u8,

) StringBuilder

Creates a StringBuilder around the provided buf. buf must have been created by the provided allocator.

fn fromReader(
	allocator: std.mem.Allocator,

	// Typically an std.io.Reader, but can be any type that has a read([]u8) !size method
	reader: anytype,

	// {.max_size = usize, .buffer_size = usize}
	opts: FromReaderOpts

) !StringBuilder

Creates and populates a StringBuilder by reading from the reader.

Options:

  • max_size: usize - Will return an error.TooBig if more than the specified value is read. Default: std.math.maxInt(usize).
  • buffer_size: usize - For the sake of efficiency, the reader is read directly into the StringBuilder's underlying buffer (there is no intermediate buffer). `buffer_size` controls the (a) initial size and (b) growth of the underlying buffer. Defaults to `8192`. Is forced to 64 when the value is less than 64

Returns the length of the written data.

Releases the StringBuilder back to the pool. This method is only valid when the StringBuilder was retrieved from pool.acquire(). The StringBuilder should not be used after this is called.

Skips the specified number of bytes and returns a View that can be used to backfill the skipped data.

count can be larger than the existing buffer length, in which case skip will attempt to grow the buffer.

Returns the bytes written. Subsequent modifications to the StringBuilder may invalidate the returned data

Returns the bytes written as a null-terminated string. Subsequent modifications to the StringBuilder may invalidate the returned data

Truncates the data by n bytes. If n is greater than the length of the buffer, the length is set to 0. Does not free memory.

Appends data, growing the underlying buffer if needed

Appends data assuming that the underlying buffer has enough free space. This method is slightly faster than write but is unsafe and should only be used in conjunction with ensureUnusedCapacity.

Appends the byte, growing the underlying buffer if needed

Appends the byte assuming that the underlying buffer has enough free space. This method is slightly faster than writeByte but is unsafe and should only be used in conjunction with ensureUnusedCapacity.

Appends the byte n times, growing the underlying buffer if needed.

Returns an std.io.Writer. This allows the StringBuilder to receive writes from a number of std and third party libraries, such as std.json.stringify.

The StringBuilder exposes a variety of methods for writing integers. There's overlap between some of these functions in order to accommodate various styles/preferences.

Writes an unsigned integer using the encoding defined by sb.endian.

Variants: writeU32 and writeU64

Writes an signed integer using the encoding defined by sb.endian.

Variants: writeI32 and writeI64

Writes an unsigned integer using little endian encoding.

Variants: writeU32Little and writeU64Little

Writes an signed integer using little endian encoding.

Variants: writeI32Little and writeI64Little

Writes an unsigned integer using big endian encoding.

Variants: writeU32Big and writeU64Big

Writes an signed integer using big endian encoding.

Variants: writeI32Big and writeI64Big

Writes an integer using the encoding defined by sb.endian. Type type of value determines how the value is encoded.

It is a compile-time error to pass a comptime_int. You must cast the constant.

Writes an integer using the specified endianness. Type type of value determines how the value is encoded.

It is a compile-time error to pass a comptime_int. You must cast the constant.

When the sb.skip() method is called, a View is returned to backfill the skipped space. This is API exists to "reserve" space within the buffer for data unknown until a later point. The typical example is a length-prefixed message where the length is only known after the message has been written.

The View exposes all of the same write* methods as the StringBuilder, but will not grow the buffer and does no bound-checking.

zul.StringBuilder.Pool is a thread-safe pool of zul.StringBuilders that will grow as needed. Each zul.StringBuilder has a re-usable static buffer which will be used before any dynamic allocations are needed.

// create a pool of 10 StringBuilders
// with each StringBuilder having a re-usable static buffer of 1024 bytes.
// pool is thread-safe
var pool = zul.StringBuilder.Pool.init(t.allocator, 10, 1024);
defer pool.deinit();

var sb = try pool.acquire();
defer sb.release();

// Use sb as documented above
sb.write("hello world");
...

Creates a StringBuilder.Pool. The pool will have pool_size StringBuilders, and each StringBuilder will maintain a static buffer of static_size bytes.

Releases all allocated memory. The Pool and any of its StringBuilders should not be used after this is called.

Retrieves a StringBuilder from the pool. If the pool is empty, will attempt to create a new one. This method is thread-safe.

Releases the StringBuilder back into the pool. If the pool is full, the StringBuilder is freed. This method is thread-safe. Most implentations will prefer to call release() on the StringBuilder directly.