A poolable string builder (aka string buffer) for Zig
0

Configure Feed

Select the types of activity you want to include in your feed.

rename stringbuilder -> buffer for easier use

Karl Seguin (Jul 23, 2023, 8:56 AM +0800) edc08a91 b6513022

+63 -63
+3 -3
build.zig
··· 4 4 const target = b.standardTargetOptions(.{}); 5 5 const optimize = b.standardOptimizeOption(.{}); 6 6 7 - _ = b.addModule("string_builder", .{ 8 - .source_file = .{ .path = "src/string_builder.zig" }, 7 + _ = b.addModule("buffer", .{ 8 + .source_file = .{ .path = "src/buffer.zig" }, 9 9 }); 10 10 11 11 const lib_test = b.addTest(.{ 12 - .root_source_file = .{ .path = "src/string_builder.zig" }, 12 + .root_source_file = .{ .path = "src/buffer.zig" }, 13 13 .target = target, 14 14 .optimize = optimize, 15 15 });
+13 -13
readme.md
··· 1 - # StringBuilder For Zig 1 + # String Builder / Buffer For Zig 2 2 3 3 Behaves a lot like a `std.ArrayList(u8)` but with a cleaner interface and pooling capabilities. 4 4 5 5 ```zig 6 - const StringBuilder = @import("string_builder").StringBuilder; 6 + const Buffer = @import("buffer").Buffer; 7 7 8 8 // Starts off with a static buffer of 100 bytes 9 9 // If you go over this, memory will be dynamically allocated ··· 11 11 // those 100 bytes (aka the static portion of the buffer) are 12 12 // re-used when pooling is used 13 13 14 - var sb = try new StringBuilder(allocator, 100); 14 + var buf = try new Buffer(allocator, 100); 15 15 16 - try sb.writeByte('o'); 17 - try sb.write("ver 9000!1"); 18 - sb.truncate(1); 16 + try buf.writeByte('o'); 17 + try buf.write("ver 9000!1"); 18 + buf.truncate(1); 19 19 20 - sb.len(); // 10 21 - sb.string(); // "over 9000!" 20 + buf.len(); // 10 21 + buf.string(); // "over 9000!" 22 22 ``` 23 23 24 - You can call `sb.writer()` to get an `std.io.Writer`. 24 + You can call `buf.writer()` to get an `std.io.Writer`. 25 25 26 26 ## Pooling 27 27 ··· 29 29 const Pool = @import("string_builder").Pool; 30 30 31 31 32 - // Creates pool of 100 StringBuilders, each configured with a static buffer 32 + // Creates pool of 100 Buffers, each configured with a static buffer 33 33 // of 10000 bytes 34 34 var pool = try Pool.init(allocator, 100, 10000); 35 - var sb = try pool.acquire(); 36 - defer pool.release(sb); 35 + var buf = try pool.acquire(); 36 + defer pool.release(buf); 37 37 ``` 38 38 39 - The `Pool` is thread-safe. The `StringBuilder` is not thread safe. 39 + The `Pool` is thread-safe. The `Buffer` is not thread safe. 40 40 41 41 For a more advanced use case, `pool.acquireWithAllocator(std.mem.Allocator)` can be used. This has a specific purpose: to allocate the static buffer upfront using [probably] a general purpose allocator, but for any dynamic allocation to happen with a [probably] arena allocator. This is meant for the case where an arena allocator is available which outlives the checked out buffer. In such cases, using the arena allocator for any potential dynamic allocation by the buffer will offer better performance.
+23 -23
src/pool.zig
··· 1 1 const std = @import("std"); 2 2 const builtin = @import("builtin"); 3 3 4 - const StringBuilder = @import("string_builder.zig").StringBuilder; 4 + const Buffer = @import("buffer.zig").Buffer; 5 5 6 6 const Mutex = std.Thread.Mutex; 7 7 const Allocator = std.mem.Allocator; ··· 10 10 mutex: Mutex, 11 11 available: usize, 12 12 allocator: Allocator, 13 - builder_size: usize, 14 - builders: []*StringBuilder, 13 + buffer_size: usize, 14 + buffers: []*Buffer, 15 15 16 - pub fn init(allocator: Allocator, pool_size: u16, builder_size: usize) !Pool { 17 - const builders = try allocator.alloc(*StringBuilder, pool_size); 16 + pub fn init(allocator: Allocator, pool_size: u16, buffer_size: usize) !Pool { 17 + const buffers = try allocator.alloc(*Buffer, pool_size); 18 18 19 19 for (0..pool_size) |i| { 20 - var sb = try allocator.create(StringBuilder); 21 - sb.* = try StringBuilder.init(allocator, builder_size); 22 - builders[i] = sb; 20 + var sb = try allocator.create(Buffer); 21 + sb.* = try Buffer.init(allocator, buffer_size); 22 + buffers[i] = sb; 23 23 } 24 24 25 25 return Pool{ 26 26 .mutex = Mutex{}, 27 - .builders = builders, 27 + .buffers = buffers, 28 28 .allocator = allocator, 29 29 .available = pool_size, 30 - .builder_size = builder_size 30 + .buffer_size = buffer_size 31 31 }; 32 32 } 33 33 34 34 pub fn deinit(self: *Pool) void { 35 35 const allocator = self.allocator; 36 - for (self.builders) |sb| { 36 + for (self.buffers) |sb| { 37 37 sb.deinit(); 38 38 allocator.destroy(sb); 39 39 } 40 - allocator.free(self.builders); 40 + allocator.free(self.buffers); 41 41 } 42 42 43 - pub fn acquire(self: *Pool) !*StringBuilder { 43 + pub fn acquire(self: *Pool) !*Buffer { 44 44 return self.acquireWithAllocator(self.allocator); 45 45 } 46 46 47 - pub fn acquireWithAllocator(self: *Pool, dyn_allocator: Allocator) !*StringBuilder { 47 + pub fn acquireWithAllocator(self: *Pool, dyn_allocator: Allocator) !*Buffer { 48 48 self.mutex.lock(); 49 49 50 - const builders = self.builders; 50 + const buffers = self.buffers; 51 51 const available = self.available; 52 52 if (available == 0) { 53 53 // dont hold the lock over factory 54 54 self.mutex.unlock(); 55 55 const allocator = self.allocator; 56 56 57 - const sb = try allocator.create(StringBuilder); 58 - sb.* = try StringBuilder.init(allocator, self.builder_size); 57 + const sb = try allocator.create(Buffer); 58 + sb.* = try Buffer.init(allocator, self.buffer_size); 59 59 if (comptime builtin.is_test) sb.buf[0] = 0; 60 60 sb._da = dyn_allocator; 61 61 return sb; 62 62 } 63 63 const index = available - 1; 64 - const sb = builders[index]; 64 + const sb = buffers[index]; 65 65 self.available = index; 66 66 self.mutex.unlock(); 67 67 sb._da = dyn_allocator; 68 68 return sb; 69 69 } 70 70 71 - pub fn release(self: *Pool, sb: *StringBuilder) void { 71 + pub fn release(self: *Pool, sb: *Buffer) void { 72 72 sb.reset(true); 73 73 self.mutex.lock(); 74 74 75 - var builders = self.builders; 75 + var buffers = self.buffers; 76 76 const available = self.available; 77 - if (available == builders.len) { 77 + if (available == buffers.len) { 78 78 self.mutex.unlock(); 79 79 const allocator = self.allocator; 80 80 sb.deinit(); 81 81 allocator.destroy(sb); 82 82 return; 83 83 } 84 - builders[available] = sb; 84 + buffers[available] = sb; 85 85 self.available = available + 1; 86 86 self.mutex.unlock(); 87 87 } ··· 129 129 defer p.deinit(); 130 130 131 131 // initialize this to 0 since we're asserting that it's 0 132 - for (p.builders) |sb| { 132 + for (p.buffers) |sb| { 133 133 sb.buf[0] = 0; 134 134 } 135 135
+24 -24
src/string_builder.zig src/buffer.zig
··· 3 3 const Allocator = std.mem.Allocator; 4 4 pub const Pool = @import("pool.zig").Pool; 5 5 6 - pub const StringBuilder = struct { 6 + pub const Buffer = struct { 7 7 // Two allocators! This is largely a feature meant to be used with the Pool. 8 8 // Imagine you have a pool of 100 StringBuilders. Each one has a static buffer 9 9 // of 2K, allocated with a general purpose allocator. We store that in _a. ··· 30 30 // points to either static or dynamic, 31 31 buf: []u8, 32 32 33 - pub fn init(allocator: Allocator, size: usize) !StringBuilder { 33 + pub fn init(allocator: Allocator, size: usize) !Buffer { 34 34 const static = try allocator.alloc(u8, size); 35 35 return .{ 36 36 ._a = allocator, ··· 42 42 }; 43 43 } 44 44 45 - pub fn deinit(self: StringBuilder) void { 45 + pub fn deinit(self: Buffer) void { 46 46 const allocator = self._a; 47 47 allocator.free(self.static); 48 48 if (self.dynamic) |dyn| { ··· 50 50 } 51 51 } 52 52 53 - pub fn reset(self: *StringBuilder, clear_dynamic: bool) void { 53 + pub fn reset(self: *Buffer, clear_dynamic: bool) void { 54 54 self.pos = 0; 55 55 if (clear_dynamic) { 56 56 if (self.dynamic) |dyn| { ··· 62 62 } 63 63 } 64 64 65 - pub fn len(self: StringBuilder) usize { 65 + pub fn len(self: Buffer) usize { 66 66 return self.pos; 67 67 } 68 68 69 - pub fn string(self: StringBuilder) []const u8 { 69 + pub fn string(self: Buffer) []const u8 { 70 70 return self.buf[0..self.pos]; 71 71 } 72 72 73 - pub fn truncate(self: *StringBuilder, n: usize) void { 73 + pub fn truncate(self: *Buffer, n: usize) void { 74 74 const pos = self.pos; 75 75 if (n >= pos) { 76 76 self.pos = 0; ··· 79 79 self.pos = pos - n; 80 80 } 81 81 82 - pub fn writeByte(self: *StringBuilder, b: u8) !void { 82 + pub fn writeByte(self: *Buffer, b: u8) !void { 83 83 try self.ensureUnusedCapacity(1); 84 84 self.writeByteAssumeCapacity(b); 85 85 } 86 86 87 - pub fn writeByteAssumeCapacity(self: *StringBuilder, b: u8) void { 87 + pub fn writeByteAssumeCapacity(self: *Buffer, b: u8) void { 88 88 const pos = self.pos; 89 89 self.buf[pos] = b; 90 90 self.pos = pos + 1; 91 91 } 92 92 93 - pub fn writeByteNTimes(self: *StringBuilder, b: u8, n: usize) !void { 93 + pub fn writeByteNTimes(self: *Buffer, b: u8, n: usize) !void { 94 94 try self.ensureUnusedCapacity(n); 95 95 const pos = self.pos; 96 96 const buf = self.buf; ··· 100 100 self.pos = pos + n; 101 101 } 102 102 103 - pub fn write(self: *StringBuilder, data: []const u8) !void { 103 + pub fn write(self: *Buffer, data: []const u8) !void { 104 104 try self.ensureUnusedCapacity(data.len); 105 105 return self.writeAssumeCapacity(data); 106 106 } 107 107 108 - pub fn writeAssumeCapacity(self: *StringBuilder, data: []const u8) void { 108 + pub fn writeAssumeCapacity(self: *Buffer, data: []const u8) void { 109 109 const pos = self.pos; 110 110 const end_pos = pos + data.len; 111 111 std.mem.copyForwards(u8, self.buf[pos..end_pos], data); 112 112 self.pos = end_pos; 113 113 } 114 114 115 - pub fn ensureUnusedCapacity(self: *StringBuilder, n: usize) !void { 115 + pub fn ensureUnusedCapacity(self: *Buffer, n: usize) !void { 116 116 return self.ensureTotalCapacity(self.pos + n); 117 117 } 118 118 119 - pub fn ensureTotalCapacity(self: *StringBuilder, required_capacity: usize) !void { 119 + pub fn ensureTotalCapacity(self: *Buffer, required_capacity: usize) !void { 120 120 const buf = self.buf; 121 121 if (required_capacity <= buf.len) { 122 122 return; ··· 147 147 } 148 148 } 149 149 150 - pub fn copy(self: StringBuilder, allocator: Allocator) ![]const u8 { 150 + pub fn copy(self: Buffer, allocator: Allocator) ![]const u8 { 151 151 const pos = self.pos; 152 152 var c = try allocator.alloc(u8, pos); 153 153 @memcpy(c, self.buf[0..pos]); 154 154 return c; 155 155 } 156 156 157 - pub fn writer(self: *StringBuilder) Writer.IOWriter { 157 + pub fn writer(self: *Buffer) Writer.IOWriter { 158 158 return .{.context = Writer.init(self)}; 159 159 } 160 160 161 161 pub const Writer = struct { 162 - sb: *StringBuilder, 162 + sb: *Buffer, 163 163 164 164 pub const Error = Allocator.Error; 165 165 pub const IOWriter = std.io.Writer(Writer, error{OutOfMemory}, Writer.write); 166 166 167 - fn init(sb: *StringBuilder) Writer { 167 + fn init(sb: *Buffer) Writer { 168 168 return .{.sb = sb}; 169 169 } 170 170 ··· 181 181 } 182 182 183 183 test "growth" { 184 - var sb = try StringBuilder.init(t.allocator, 10); 184 + var sb = try Buffer.init(t.allocator, 10); 185 185 defer sb.deinit(); 186 186 187 187 // we reset at the end of the loop, and things should work the exact same ··· 215 215 } 216 216 217 217 test "truncate" { 218 - var sb = try StringBuilder.init(t.allocator, 10); 218 + var sb = try Buffer.init(t.allocator, 10); 219 219 defer sb.deinit(); 220 220 221 221 sb.truncate(100); ··· 237 237 } 238 238 239 239 test "reset without clear" { 240 - var sb = try StringBuilder.init(t.allocator, 5); 240 + var sb = try Buffer.init(t.allocator, 5); 241 241 defer sb.deinit(); 242 242 243 243 ··· 265 265 const aa = arena.allocator(); 266 266 267 267 for (1..100) |_| { 268 - var sb = try StringBuilder.init(t.allocator, random.uintAtMost(u16, 1000) + 1); 268 + var sb = try Buffer.init(t.allocator, random.uintAtMost(u16, 1000) + 1); 269 269 defer sb.deinit(); 270 270 271 271 for (1..100) |_| { ··· 281 281 } 282 282 283 283 test "writer" { 284 - var sb = try StringBuilder.init(t.allocator, 10); 284 + var sb = try Buffer.init(t.allocator, 10); 285 285 defer sb.deinit(); 286 286 287 287 try std.json.stringify(.{.over = 9000, .spice = "must flow", .ok = true}, .{}, sb.writer()); ··· 289 289 } 290 290 291 291 test "copy" { 292 - var sb = try StringBuilder.init(t.allocator, 10); 292 + var sb = try Buffer.init(t.allocator, 10); 293 293 defer sb.deinit(); 294 294 295 295 try sb.write("hello!!");