zig로 wc 만들기
요즘 Zig를 찍먹해보고 있다. 관심을 갖게 된 계기는 JS진영의 bun이라는 런타임이 zig라는 얘기를 들어서 였다.
문법을 보다 보니 Go 같기도 하고, C의 향수도 느껴지는게 취향에 맞아서 이것저것 해 보면서 언어를 배우고 있다.
Hello world를 해봤다면 다음은 당연히 wc를 짜 봐야 하는게 아닐까? 그래서 zig에서 wc를 마들어 보았다.
역사의 고전 를 본 사람이라면 wc라는 프로그램을 짜는 것에 익숙할거다. 간단하지만 변수, 데이터타입, 제어문 등 기본적인 프로그래밍 문법들을 익힐 수 있어서, 새로운 언어를 배울 때 나는 종종 wc를 만들곤 한다.
The C Programming Language 스타일로 wc를 만들어 보자.
The C Programming Language의 wc
#include <stdio.h>
#define IN 1
#define OUT 0
int main()
{
int c, nl, nw, nc, state;
state = OUT;
nl = nw = nc = 0;
while ((c = getchar()) != EOF)
{
nc++;
if (c == '\n')
nl++;
if (c == ' ' || c == '\n' || c == '\t')
state = OUT;
else if (state == OUT)
{
state = IN;
nw++;
}
}
printf("%d %d %d\n", nl, nw, nc);
}
줄 수, 단어 수, 문자 수를 세는 wc 프로그램이다. 입력은 표준입력으로 받으며, 출력은 표준출력으로 한다.
이제 zig로 한번 만들어본다.
Zig로 wc 만들기
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const stdin = std.io.getStdIn().reader();
var nl: usize = 0;
var nw: usize = 0;
var nc: usize = 0;
const State = enum {
In,
Out,
};
var state = State.Out;
while (stdin.readByte()) |c| {
nc += 1;
if (c == '\n') {
nl += 1;
}
if (c == ' ' or c == '\n' or c == '\t') {
state = State.Out;
} else if (state == State.Out) {
nw += 1;
state = State.In;
}
} else |err| switch (err) {
error.EndOfStream => {}, // EOF
else => return err,
}
try stdout.print("{d} {d} {d}\n", .{ nl, nw, nc });
}
우선 C 예시와는 다르게 enum을 사용했고(enum이 편하다고 생각했음), 표준입력을 read할 때 error를 처리하는 구절에 변화가 생겼다.
zig에서는 옵셔널한 값을 처리하는 로직이 흔히 사용된다.
예를 들어,
fn readByte(self: Self) NoEofError!u8
이 함수의 경우,
- 값이 있다면 u8을 반환하고,
- 값이 없다면
NoEofError
라는 에러 집합 중 하나를 반환한다.
그리고,
if (stdin.readByte()) |c| {
// c가 u8인 경우
} else |err| switch (err) {
error.EndOfStream => {}, // EOF
else => return err,
}
이 경우 if문은 u8이 있는 경우에 실행되고, c라는 변수에 u8 값을 할당해 블록을 실행한다.
한편, else 블록은 에러가 발생한 경우에 실행하고 err
라는 변수에 에러 값을 할당해 블록을 실행한다.
테스트를 위해서 lorem ipsum을 활용해 test.txt라는 파일을 만들어서 실행해봤다.
먼저 실제 wc 프로그램을 실행해본다.
$ wc < test.txt
9 93 625
다음으로 zig로 짠 wc를 실행해본다.
$ zig run wc.zig < test.txt
9 93 625
잘 동작하는 것 같다.
zig 문법은 C와 유사하지만, 현대적이고 사용성이 좋다. C를 사랑하는 사람에게, C에서 불편했던 점들을 해결한 좋은 대안이 되는 언어라는 생각이 든다.
Zig의 장점
- C 수준의 성능과 안정성: 포인터, 메모리, syscall 등을 직접 다룰 수 있어 C와 유사한 성능을 낼 수 있다. 한편 null, error, optional 등의 개념을 도입해 안정성을 높였다.
- 에러 핸들링이 명시적이고 깔끔: try, catch, if |x| else |err| 같은 문법으로 에러가 보이지 않게 숨어있지 않음. try 문으로 error 전파 가능.
- comptime: 함수나 로직을 컴파일 타임에 실행 가능. comptime 덕분에 매크로 없이도 제네릭, 코드 생성, 최적화 가능
- 의존성 없는 빌드: zig만으로 컴파일. zig는 C 컴파일러를 내장. C 코드도 zig에서 바로 통합 가능
- 단순하고 예측 가능한 문법: undefined behavior를 최대한 방지하고 문법도 명확. 자동 메모리 관리 없음 → 성능과 제어력 ↑.
결론
물론 이런 점들이 경우에 따라서는 단점이 될 수도 있다. Rust에 비하면 C와 유사한 문법이라 이해가 쉽지만, 그런 만큼 특별한 장점을 찾기 어려운 언어일 수도 있다고 생각한다.
ChatGPT한테 요약을 맡겨본 Rust vs Zig 비교다.
구분 | Rust | Zig |
---|---|---|
메모리 관리 | 소유권 시스템으로 안전하게 자동 관리 | 자동 메모리 관리 없음, 직접 관리 |
안전성 | 메모리 안전과 데이터 경쟁 방지 강제 | 안전성 직접 관리, undefined behavior 최소화 |
문법 | 복잡하지만 강력한 타입 시스템 | 단순하고 명확한 문법 |
컴파일 타임 | 매크로, 제네릭 등 복잡한 기능 지원 | comptime 로 코드 생성과 최적화 특화 |
빌드/툴링 | Cargo라는 강력한 패키지 매니저 사용 | 내장 빌드 시스템과 크로스 컴파일 간편 |
생태계 | 매우 크고 활발 | 아직 작고 성장 중 |
C호환성 | 제한적 | 뛰어남, C코드 쉽게 통합 가능 |
Rust vs Zig 한 줄 요약
- Rust: 안전성과 풍부한 기능을 원할 때
- Zig: 간단하고 명확한 시스템 프로그래밍, C와 통합이 중요할 때
Zig의 미래
끝으로 zig를 사용하는 프로젝트들을 몇 개 찾아봤다.
- Ghostty: 깔끔한 터미널 에뮬레이터로 iterm2를 버리고 요즘 애용하고 있다. 33k stars.
- Bun: TypeScript를 네이티브로 지원하는 JavaScript 런타임. Deno와 Node.js의 대안으로 주목받고 있다. 79k stars.
- tigerbeetle: 재무/금융 트랜잭션 데이터베이스. 13k stars.