
Zig Workshop #1: วางโครง CLI ที่รันได้จริงด้วย build.zig ของ 0.16
สิบบทแรกเราเรียนภาษา ตั้งแต่บทนี้เราจะสร้างของจริง
สิ่งที่จะได้ตอนจบ workshop คือ zlog — เครื่องมือบรรทัดคำสั่งที่อ่าน access log ของ nginx แล้วสรุปให้ว่า path ไหนถูกเรียกบ่อยสุด อัตราความผิดพลาดเท่าไหร่ ขนาด response ที่เปอร์เซ็นไทล์ 95 เป็นเท่าไหร่ ทำงานได้ทั้ง macOS, Linux และ Windows และเร็วพอที่จะเอาไปใช้จริง
ทำไมเลือกโจทย์นี้: มันบังคับให้ใช้ของที่เป็นจุดแข็งของ Zig ครบทุกอย่าง — อ่านข้อมูลแบบ streaming, แยกข้อความโดยไม่ copy, จัดการหน่วยความจำเอง, comptime และการคอมไพล์ข้ามแพลตฟอร์ม โดยไม่ต้องพึ่ง API ที่ยังไม่นิ่งเลยสักตัว
โค้ดทั้งหมดอยู่ที่ kangana1024/showkhun-zig-workshop แต่ละบทมี branch ของตัวเอง บทนี้คือ step-01-skeleton
เป้าหมายของบทนี้
zig build run -- --version
# zlog 0.1.0 (zig 0.16.0)
เล็กมาก แต่มันบังคับให้เราตอบคำถามพื้นฐานทุกข้อของโปรเจกต์ Zig ให้ครบ
เริ่มจาก zig init
mkdir zlog
cd zlog
zig init
ผลจริงที่ได้ใน 0.16:
info: created build.zig
info: created build.zig.zon
info: created src/main.zig
info: created src/root.zig
info: see `zig build --help` for a menu of options
zig init สร้างมาให้ทั้งไฟล์ library (src/root.zig) และไฟล์โปรแกรม (src/main.zig) ซึ่งเป็นโครงที่ดี — โค้ดตรรกะอยู่ฝั่ง library เพื่อให้เทสได้ ส่วน main.zig ทำหน้าที่แค่รับ argument กับพิมพ์ผล
build.zig.zon: จุดที่โปรเจกต์เก่าพัง
ไฟล์นี้คือข้อมูลประจำตัวโปรเจกต์ ของที่ zig init สร้างให้มีคอมเมนต์ยาวมาก บล็อกข้างล่างเป็นโค้ดบางส่วนที่ตัดคอมเมนต์ออกให้เหลือแก่น
.{
.name = .zlog,
.version = "0.1.0",
.fingerprint = 0x55b82e33491c94fc, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{ "build.zig", "build.zig.zon", "src", "LICENSE", "README.md" },
}
มีสองจุดที่ 0.16 เข้มขึ้นและทำให้โปรเจกต์เก่า build ไม่ผ่าน
หนึ่ง — .name ต้องเป็น enum literal เขียน .name = .zlog ไม่ใช่ .name = "zlog" แบบเดิม
สอง — ต้องมี .fingerprint เป็นรหัสประจำตัวโปรเจกต์ที่ zig init สุ่มให้ครั้งเดียวแล้ว ห้ามแก้อีก คอมเมนต์ที่ Zig ใส่ไว้บอกเหตุผลตรง ๆ ว่าการแก้ค่านี้มีนัยด้านความปลอดภัยและความน่าเชื่อถือ เพราะมันคือสิ่งที่ทำให้ระบบแยกออกว่า package ไหนเป็นเวอร์ชันใหม่ของ package ไหน
.minimum_zig_version = "0.16.0" ไม่บังคับแต่ควรใส่ เพราะชุดบทความนี้ยืนยันแล้วว่าโค้ดแบบนี้ใช้กับ 0.15 ไม่ได้
build.zig: สคริปต์ build ที่เป็นภาษา Zig
บล็อกข้างล่างเป็นโค้ดบางส่วน ตัดคอมเมนต์ยาว ๆ ที่ zig init ใส่มาออก ไฟล์เต็มอยู่ใน repo
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// module = ชุดไฟล์ + ตัวเลือกการคอมไพล์ ต้องสร้างก่อนเสมอใน 0.16
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{ .name = "zlog", .root_module = exe_mod });
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args); // ส่งต่อสิ่งที่อยู่หลัง --
const run_step = b.step("run", "รัน zlog");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest(.{ .root_module = exe_mod });
const test_step = b.step("test", "รันเทสทั้งหมด");
test_step.dependOn(&b.addRunArtifact(exe_tests).step);
}
จุดที่ทำให้ตัวอย่างเก่าทั้งหมดพัง: ใน 0.14 และก่อนหน้า คนเขียนแบบนี้ (โค้ดบางส่วน)
// โค้ดบางส่วนแบบเก่า — ใช้กับ 0.16 ไม่ได้แล้ว
const exe = b.addExecutable(.{
.name = "zlog",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
ExecutableOptions ของ 0.16 ไม่มีฟิลด์ root_source_file, target หรือ optimize แล้ว ทุกอย่างย้ายไปอยู่ใน module ที่ต้องสร้างก่อน
b.standardTargetOptions และ b.standardOptimizeOption คือสิ่งที่ทำให้เราสั่ง zig build -Dtarget=... และ -Doptimize=... ได้จากบรรทัดคำสั่ง — เดี๋ยวบทสุดท้ายจะได้ใช้หนัก
main.zig: โครงของ CLI
const std = @import("std");
const Io = std.Io;
pub const version = "0.1.0";
const Command = enum { count, top, help, version_only };
fn commandOf(arg: []const u8) Command {
if (std.mem.eql(u8, arg, "--version")) return .version_only;
return std.meta.stringToEnum(Command, arg) orelse .help;
}
fn usage(w: *Io.Writer) !void {
try w.print(
\\zlog {s} — วิเคราะห์ nginx access log
\\
\\ zlog count <file...>
\\ zlog top <file...>
\\ zlog --version
\\
, .{version});
}
pub fn main(init: std.process.Init) !u8 {
const io = init.io;
var out_buf: [4096]u8 = undefined;
var fw: Io.File.Writer = .init(.stdout(), io, &out_buf);
const w = &fw.interface;
defer w.flush() catch {}; // กัน output หายตอนออกทางไหนก็ตาม
const args = try init.minimal.args.toSlice(init.arena.allocator());
const command: Command = if (args.len > 1) commandOf(args[1]) else .help;
switch (command) {
.version_only => try w.print("zlog {s} (zig {s})\n", .{ version, @import("builtin").zig_version_string }),
.help => try usage(w),
.count, .top => try w.print("ยังไม่ได้ทำ: {t}\n", .{command}),
}
return 0;
}
test "commandOf อ่านคำสั่งจาก argument" {
try std.testing.expectEqual(Command.count, commandOf("count"));
try std.testing.expectEqual(Command.top, commandOf("top"));
try std.testing.expectEqual(Command.version_only, commandOf("--version"));
try std.testing.expectEqual(Command.help, commandOf("อะไรก็ไม่รู้"));
}
มีห้าเรื่องที่ตั้งใจใส่ไว้ตั้งแต่บรรทัดแรก
1. main(init: std.process.Init) !u8 — รับ init เพื่อเข้าถึง io, allocator และ argument และคืน u8 เพื่อคุมรหัสจบโปรแกรมเอง ซึ่งจำเป็นสำหรับเครื่องมือที่คนจะเอาไปต่อใน shell script (0 = สำเร็จ, 1 = ทำงานผิดพลาด, 2 = ใช้คำสั่งผิด)
2. defer w.flush() catch {} — วางไว้ทันทีหลังสร้าง writer ไม่ว่าโปรแกรมจะออกทางไหน output จะถูกส่งออกครบ นี่คือบั๊กที่ทุกคนเจอในการเขียน Zig 0.16 ครั้งแรก ถ้าลืมบรรทัดนี้ โปรแกรมจะเงียบสนิททั้งที่โค้ด print ทำงานแล้ว
3. init.arena.allocator() สำหรับ argument — argument อยู่จนจบโปรแกรม ไม่ต้องคืนทีละชิ้น arena ที่ Zig เตรียมให้เหมาะพอดี ไม่ต้องเขียนโค้ด free เลย
4. std.meta.stringToEnum — แปลงข้อความเป็น enum ให้เลย ไม่ต้องเขียน if เทียบทีละอัน และเมื่อเพิ่มคำสั่งใหม่ใน enum มันรองรับให้ทันที
5. {t} — ตัวระบุรูปแบบของ 0.16 ที่พิมพ์ชื่อ tag ของ enum ออกมา สั้นกว่า @tagName
รันดู
zig build test # เทสผ่านทั้งหมด
zig build run -- --version # zlog 0.1.0 (zig 0.16.0)
zig build run # ไม่มีคำสั่ง → แสดงวิธีใช้
zig build # ได้ไฟล์ที่ zig-out/bin/zlog
-- ในคำสั่ง zig build run -- --version สำคัญ: ทุกอย่างหลัง -- คือ argument ของโปรแกรมเรา ส่วนก่อนหน้าเป็นของ zig build
สรุปบทนี้
zig initของ 0.16 สร้างทั้งmain.zigและroot.zig— วางตรรกะไว้ฝั่ง library เพื่อให้เทสได้build.zig.zonบังคับ.nameเป็น enum literal และต้องมี.fingerprintที่ห้ามแก้build.zigของ 0.16 ต้องสร้าง module ก่อน แล้วค่อยเอาไปทำ executablemain(init: std.process.Init) !u8ให้ทั้งio, allocator, argument และคุมรหัสจบโปรแกรมได้defer w.flush()ตั้งแต่บรรทัดที่สร้าง writer
บทหน้าเราจะอ่านไฟล์จริง — ทีละบรรทัดแบบ streaming ที่ไฟล์ 10 กิกะไบต์ก็ไม่ทำให้แรมเต็ม พร้อมจัดการเรื่องบรรทัดแบบ Windows ที่คนลืมกันประจำ
Navigation
- ก่อนหน้า: บทที่ 10: Zig เด่นตรงไหน
- ถัดไป: Workshop #2: อ่านไฟล์ทีละบรรทัดแบบ streaming
- โค้ดบทนี้: step-01-skeleton
- ดูทั้งชุด: บทความแท็ก zig