add zig 0.15+ patterns notes

synthesized from leaflet-search, find-bufo, zql, pollz

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+413
languages
ziglang
+413
languages/ziglang/README.md
··· 1 + # zig 0.15+ 2 + 3 + notes on zig 0.15+ patterns. pre-0.15 patterns are not covered here. 4 + 5 + sources: leaflet-search, find-bufo, zql, pollz (dec 2025 - jan 2026) 6 + 7 + --- 8 + 9 + ## build system 10 + 11 + ### module imports (0.15+ pattern) 12 + 13 + ```zig 14 + const exe = b.addExecutable(.{ 15 + .name = "myapp", 16 + .root_module = b.createModule(.{ 17 + .root_source_file = b.path("src/main.zig"), 18 + .imports = &.{ 19 + .{ .name = "websocket", .module = websocket.module("websocket") }, 20 + .{ .name = "zqlite", .module = zqlite.module("zqlite") }, 21 + }, 22 + }), 23 + }); 24 + ``` 25 + 26 + ### build.zig.zon 27 + 28 + ```zig 29 + .{ 30 + .name = .my_project, 31 + .version = "0.0.1", 32 + .minimum_zig_version = "0.15.0", 33 + .dependencies = .{ 34 + .websocket = .{ 35 + .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.tar.gz", 36 + .hash = "...", 37 + }, 38 + }, 39 + } 40 + ``` 41 + 42 + ### library modules 43 + 44 + ```zig 45 + const mod = b.addModule("zql", .{ 46 + .root_source_file = b.path("src/root.zig"), 47 + .target = target, 48 + .optimize = optimize, 49 + }); 50 + 51 + const tests = b.addTest(.{ .root_module = mod }); 52 + ``` 53 + 54 + --- 55 + 56 + ## memory management 57 + 58 + ### general purpose allocator (application lifetime) 59 + 60 + ```zig 61 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 62 + defer _ = gpa.deinit(); 63 + const allocator = gpa.allocator(); 64 + ``` 65 + 66 + ### arena allocator (request-scoped) 67 + 68 + ```zig 69 + fn handleRequest(request: *http.Server.Request) !void { 70 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 71 + defer arena.deinit(); 72 + const alloc = arena.allocator(); 73 + // all allocations freed when arena deinits 74 + } 75 + ``` 76 + 77 + ### arraylist lifecycle 78 + 79 + ```zig 80 + var list: std.ArrayList(u8) = .{}; 81 + defer list.deinit(allocator); 82 + try list.appendSlice(allocator, data); 83 + ``` 84 + 85 + --- 86 + 87 + ## error handling 88 + 89 + ### try with propagation 90 + 91 + ```zig 92 + const result = try doSomething(); // propagates error 93 + ``` 94 + 95 + ### catch with specific error handling 96 + 97 + ```zig 98 + const n = tls_client.reader.stream(&w, .limited(len)) catch |err| { 99 + if (err == error.EndOfStream) break :outer; 100 + return error.Failed; 101 + }; 102 + ``` 103 + 104 + ### catch with default 105 + 106 + ```zig 107 + const port = std.fmt.parseInt(u16, port_str, 10) catch 443; 108 + ``` 109 + 110 + ### labeled blocks for fallback values 111 + 112 + ```zig 113 + const count: i64 = blk: { 114 + const row = db.conn.row("SELECT COUNT(*) FROM x", .{}) catch break :blk 0; 115 + if (row) |r| { 116 + defer r.deinit(); 117 + break :blk r.int(0); 118 + } 119 + break :blk 0; 120 + }; 121 + ``` 122 + 123 + --- 124 + 125 + ## concurrency 126 + 127 + ### thread pool 128 + 129 + ```zig 130 + var pool: Thread.Pool = undefined; 131 + try pool.init(.{ .allocator = allocator, .n_jobs = 16 }); 132 + defer pool.deinit(); 133 + 134 + pool.spawn(handleConnection, .{conn}) catch |err| { 135 + conn.stream.close(); 136 + }; 137 + ``` 138 + 139 + ### mutex for shared state 140 + 141 + ```zig 142 + pub var mutex: Thread.Mutex = .{}; 143 + 144 + pub fn insert(data: []const u8) !void { 145 + mutex.lock(); 146 + defer mutex.unlock(); 147 + // critical section 148 + } 149 + ``` 150 + 151 + ### atomic values 152 + 153 + ```zig 154 + posts_checked: std.atomic.Value(u64) = .init(0), 155 + 156 + // increment 157 + _ = self.posts_checked.fetchAdd(1, .monotonic); 158 + 159 + // read 160 + const count = self.posts_checked.load(.monotonic); 161 + ``` 162 + 163 + ### background thread 164 + 165 + ```zig 166 + const thread = try Thread.spawn(.{}, consumer, .{allocator}); 167 + ``` 168 + 169 + ### exponential backoff 170 + 171 + ```zig 172 + var backoff: u64 = 1; 173 + const max_backoff: u64 = 60; 174 + 175 + while (true) { 176 + connect() catch {}; 177 + posix.nanosleep(backoff, 0); 178 + backoff = @min(backoff * 2, max_backoff); 179 + } 180 + ``` 181 + 182 + --- 183 + 184 + ## networking 185 + 186 + ### tcp + tls (0.15 pattern) 187 + 188 + ```zig 189 + const stream = net.tcpConnectToHost(allocator, host, 443) catch return error.Failed; 190 + defer stream.close(); 191 + 192 + var arena = std.heap.ArenaAllocator.init(allocator); 193 + defer arena.deinit(); 194 + const aa = arena.allocator(); 195 + 196 + var ca_bundle: std.crypto.Certificate.Bundle = .{}; 197 + ca_bundle.rescan(aa) catch return error.Failed; 198 + 199 + const buf_len = std.crypto.tls.max_ciphertext_record_len; 200 + const buf = aa.alloc(u8, buf_len * 4) catch return error.Failed; 201 + 202 + var stream_writer = stream.writer(buf.ptr[0..buf_len][0..buf_len]); 203 + var stream_reader = stream.reader(buf.ptr[buf_len .. 2 * buf_len][0..buf_len]); 204 + 205 + var tls_client = tls.Client.init( 206 + stream_reader.interface(), 207 + &stream_writer.interface, 208 + .{ 209 + .ca = .{ .bundle = ca_bundle }, 210 + .host = .{ .explicit = host }, 211 + .read_buffer = buf.ptr[2 * buf_len .. 3 * buf_len][0..buf_len], 212 + .write_buffer = buf.ptr[3 * buf_len .. 4 * buf_len][0..buf_len], 213 + }, 214 + ) catch return error.Failed; 215 + ``` 216 + 217 + ### tls read loop (must loop until data) 218 + 219 + ```zig 220 + outer: while (total_read < response_buf.len) { 221 + var w: std.Io.Writer = .fixed(response_buf[total_read..]); 222 + while (true) { 223 + const n = tls_client.reader.stream(&w, .limited(remaining)) catch break :outer; 224 + if (n != 0) { 225 + total_read += n; 226 + break; 227 + } 228 + } 229 + } 230 + ``` 231 + 232 + ### http server 233 + 234 + ```zig 235 + var read_buffer: [8192]u8 = undefined; 236 + var write_buffer: [8192]u8 = undefined; 237 + 238 + var reader = conn.stream.reader(&read_buffer); 239 + var writer = conn.stream.writer(&write_buffer); 240 + 241 + var server = http.Server.init(reader.interface(), &writer.interface); 242 + 243 + while (true) { 244 + var request = server.receiveHead() catch return; 245 + handleRequest(&request) catch return; 246 + if (!request.head.keep_alive) return; 247 + } 248 + ``` 249 + 250 + ### socket timeouts 251 + 252 + ```zig 253 + const timeout = std.mem.toBytes(posix.timeval{ .sec = 30, .usec = 0 }); 254 + try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout); 255 + try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); 256 + ``` 257 + 258 + --- 259 + 260 + ## json 261 + 262 + ### parsing 263 + 264 + ```zig 265 + const parsed = json.parseFromSlice(json.Value, allocator, data, .{}) catch return; 266 + defer parsed.deinit(); 267 + 268 + const root = parsed.value.object; 269 + const val = root.get("key") orelse return; 270 + if (val != .string) return; 271 + const s = val.string; 272 + ``` 273 + 274 + ### formatting output 275 + 276 + ```zig 277 + try writer.print("{f}", .{json.fmt(value, .{})}); 278 + ``` 279 + 280 + --- 281 + 282 + ## comptime 283 + 284 + ### type-returning functions 285 + 286 + ```zig 287 + pub fn Query(comptime sql: []const u8) type { 288 + comptime { 289 + const parsed = parser.parse(sql); 290 + return struct { 291 + pub const raw = sql; 292 + pub const positional = parsed.positional[0..parsed.positional_len]; 293 + }; 294 + } 295 + } 296 + ``` 297 + 298 + ### inline for (unrolled at comptime) 299 + 300 + ```zig 301 + inline for (params) |p| { 302 + if (!hasField(fields, p)) { 303 + @compileError("missing param :" ++ p); 304 + } 305 + } 306 + ``` 307 + 308 + ### reflection 309 + 310 + ```zig 311 + const fields = @typeInfo(Args).@"struct".fields; 312 + inline for (fields) |f| { 313 + @field(result, f.name) = @field(args, f.name); 314 + } 315 + ``` 316 + 317 + ### compile-time validation 318 + 319 + ```zig 320 + @compileError("missing param :" ++ p ++ " in args struct") 321 + ``` 322 + 323 + ### branch quota for complex parsing 324 + 325 + ```zig 326 + @setEvalBranchQuota(sql.len * 100); 327 + ``` 328 + 329 + --- 330 + 331 + ## strings 332 + 333 + ### splitting 334 + 335 + ```zig 336 + var parts = mem.splitScalar(u8, data, '/'); 337 + const first = parts.next() orelse return error.Invalid; 338 + ``` 339 + 340 + ### prefix/suffix checks 341 + 342 + ```zig 343 + if (mem.startsWith(u8, target, "/api/")) { } 344 + if (mem.endsWith(u8, id, "#atproto")) { } 345 + ``` 346 + 347 + ### searching 348 + 349 + ```zig 350 + if (mem.indexOf(u8, response, "\r\n\r\n")) |idx| { 351 + const body = response[idx + 4 ..]; 352 + } 353 + ``` 354 + 355 + ### url decoding 356 + 357 + ```zig 358 + const buf = try alloc.dupe(u8, encoded); 359 + const decoded = std.Uri.percentDecodeInPlace(buf); 360 + ``` 361 + 362 + --- 363 + 364 + ## common dependencies 365 + 366 + - **websocket.zig** (karlseguin) - websocket client with tls 367 + - **zqlite.zig** (karlseguin) - sqlite wrapper 368 + 369 + --- 370 + 371 + ## idioms 372 + 373 + ### defer for cleanup 374 + 375 + ```zig 376 + var arena = std.heap.ArenaAllocator.init(allocator); 377 + defer arena.deinit(); 378 + ``` 379 + 380 + ### errdefer for error cleanup 381 + 382 + ```zig 383 + const resource = try allocate(); 384 + errdefer deallocate(resource); 385 + // if error after this, resource is cleaned up 386 + ``` 387 + 388 + ### optional access with type check 389 + 390 + ```zig 391 + if (obj.get("key")) |val| { 392 + if (val == .string) { 393 + use(val.string); 394 + } 395 + } 396 + ``` 397 + 398 + ### environment variables with defaults 399 + 400 + ```zig 401 + const host = std.posix.getenv("HOST") orelse "localhost"; 402 + ``` 403 + 404 + ### multi-line strings 405 + 406 + ```zig 407 + const sql = 408 + \\CREATE TABLE IF NOT EXISTS polls ( 409 + \\ uri TEXT PRIMARY KEY, 410 + \\ text TEXT NOT NULL 411 + \\) 412 + ; 413 + ```