-
C 코드 컴파일과 크로스 컴파일 기능을 기본 제공하는 Zig 컴파일러는, 45년 경력의 저자가 경험한 언어 중 가장 놀라운 언어임
-
컴파일 타임 실행, 임의 비트 크기 변수, 테스트 블록 환경 등 독특한 기능으로 단순한 C/C++ 대체를 넘어 완전히 새로운 프로그래밍 방식 제공
-
타입 추론을 통한 변수 선언, 익명 구조체, 레이블 브레이크 등 간결하고 명확한 문법으로 빠른 학습 가능
-
테스트 블록을 통한 독립적 모듈 테스트와 @breakpoint 내장 함수로 최적화된 코드 디버깅 지원
-
비트 필드와 비트 연산을 활용한 저수준 프로그래밍 지원으로 효율성과 견고성을 동시에 달성하며, 인터프리터 언어의 장점을 컴파일 언어에 통합
서문
- 45년 경력 중 Zig만큼 놀라운 언어는 없었음
- Zig는 단순한 새로운 언어가 아니라, 프로그래밍 방식을 근본적으로 바꾸는 도구
- C나 C++을 대체하는 수준으로만 보는 것은 큰 과소평가임
- 이 글의 목적은 Zig의 간단하면서도 매력적인 기능을 소개하고, 프로그래머가 빠르게 시작할 수 있도록 돕는 것
- 산업계에서 Zig의 수용성에 영향을 미치는 더 많은 기능이 존재함
Zig 컴파일러
-
C 코드 컴파일과 크로스 컴파일 기능을 별도 설정 없이 기본 제공하여 산업계에 큰 영향을 줌
- 설치는 Zinglang 다운로드 페이지에서 프로세서/OS별 컴파일러를 다운로드 후 압축 해제하여 원하는 디렉토리에 복사
- Windows 10에서는 x86_64 zip 파일을 "Program Files"에 복사하고, 루트 디렉토리 이름을 "zig-windows-x86_64"로 변경하여 버전 업데이트 시 Path 환경 변수 수정 불필요
- Path 환경 변수에 루트 디렉토리 경로 추가 후 CLI 모드에서 컴파일러 사용 가능
- "Hello World!" 프로그램 빌드는 공식 사이트의 "Getting Started" 섹션 참조 권장
주요 개념과 명령
변수 선언
-
변수 선언은 접근성(pub 또는 생략), var/const, 변수명으로 구성된 첫 번째 부분, 타입 선언인 두 번째 부분, 초기화인 세 번째 부분으로 구성
- 첫 번째와 세 번째 부분만 필수이며, 타입은 초기화 값으로부터 추론 가능
- 예:
var sum : usize = 0;
-
pub 없이 선언된 변수는 모듈 내부에서만 접근 가능 (C의 static 변수와 유사)
-
pub 변수 선언은 권장되지 않으며, pub 함수는 최소화하여 결합도를 낮추고 응집도를 높이는 것이 권장됨
구조체, 익명 구조체, 테스트 블록
-
.{와 }로 둘러싸인 익명 구조체 리터럴은 다른 구조체 요소 초기화 또는 요소가 초기화된 새 구조체 생성에 사용
-
.{ }는 빈 익명 구조체 리터럴
-
struct { } 형태는 구조체 선언
-
테스트 블록은 실행 파일 없이 컴파일 및 테스트 실행 가능
비트 필드
-
비트 필드는 packed struct에서 특정 크기의 타입을 가진 필드로 선언
- 포인터는 특정 비트 필드를 가리킬 수 있음
For 루프
- Zig 문법은 C보다 명확하나,
[0..8]이 아닌 열린 구간 [0..9) 사용
-
루프 변수
i의 타입 선언, 초기화, 테스트, 증가가 자동으로 처리됨
배열
-
[_]는 크기를 알 수 없는 배열을 정의하며, 요소 타입과 초기화가 뒤따름
- 예:
var grid = [_]u8{0} ** 81;는 81개의 u8 요소를 0으로 초기화
-
배열 크기는 초기화 반복 인자로부터 추론됨
- 테스트 환경에서 배열 요소를 순회하며 합산 가능
-
for 루프의
| 사이에 선언된 변수는 배열 요소와 동일한 타입으로 자동 가정
-
usize는 플랫폼의 자연 부호 없는 정수 (64비트에서 u64, 32비트에서 u32)
다항목 포인터
-
배열 포인터가 포인터 산술 연산을 사용하려면
[*]const i32처럼 명시적으로 다항목 포인터로 선언 필요
- 배열이 const여도 포인터는 var로 선언 가능
포인터 역참조
- 개별 배열 위치의 주소가 할당된 포인터는 포인터 산술로 업데이트 불가
-
포인터 역참조는
ptr.* 사용
레이블 브레이크
-
컴파일 타임에 배열 초기화 등 다양한 작업 수행 가능
-
레이블 브레이크는 블록 이름 뒤에
:를 붙이고, break로 블록에서 값 반환
-
0..는 0부터 시작하는 무한 범위
- for 루프에서 변수들은 자동 초기화 및 증가되며, 배열 마지막 위치 처리 후 루프 종료
- 배열은
undefined로 명시적으로 초기화하지 않을 수 있음
Zig의 함수
-
함수는
fn으로 선언하며 기본적으로 static (파일 내부에서만 사용)
-
pub fn으로 선언 시 다른 파일에서 import 가능
- 함수는 "inlined" 가능
- 함수 포인터는
const가 앞에 오고 함수 프로토타입이 뒤따름
Zig의 객체지향 프로그래밍
-
구조체는 함수를 가질 수 있음
- 스택 예제에서 최대 81개 요소(StkNode 타입) 저장 가능
-
++ 및 -- 연산자는 Zig에 존재하지 않으며, += 및 -= 사용
- 스택 포인터는
stk 배열의 인덱스로 사용되는 정수
-
포인터
self는 매개변수로 명시적으로 전달되지 않으며, 함수가 호출되는 스택 인스턴스의 포인터로 간접 가정
-
stack.pop()처럼 호출 시 self는 stack에 대한 포인터 (Java/C++의 this와 유사)
-
init() 함수는 스택 생성자
-
pop과 push 함수는 "inlined"
Zig 프로그램 빌드 및 실행
실행 파일 빌드
-
실행 파일 생성을 위해 프로그램 진입점을 나타내는
main 함수 필요
- 간단한 프로그램은 main 함수를 같은 파일에 포함 가능
- 모듈 독립 디버깅을 위해 파일 끝에 main 함수를 삽입한 후, 디버깅 완료 후 주석 처리 가능
- 컴파일 명령:
zig build-exe -O ReleaseFast program.zig
모듈의 테스트 블록 실행
-
Zig의 가장 우수한 기능으로, 테스트와 프로토타이핑에 사용
-
테스트 블록은
test "message" {로 시작하여 }로 끝남
- "message"는 테스트 실행 시 표시될 문자열
-
테스트 블록은 실행 파일과 독립적으로 실행되며, 최종 실행 파일은 테스트를 실행하지 않음
- 테스트 명령:
zig test module.zig
- example.zig의 테스트 블록은
set과 print 함수를 테스트하며, set은 십진수 문자열을 매개변수로 받고, print는 "Input Grid" 헤더를 출력한 후 grid를 출력
Zig의 출력
-
std.debug.print 문은 표준 Zig 라이브러리 std의 debug.zig에 있는 print 함수 호출
- 첫 번째 매개변수는 형식 문자열, 두 번째는 표시할 변수 리스트를 포함하는 익명 구조체
- 형식이 없는 경우 구조체는 비어 있음
- 기본적으로 stderr에 표시
-
C의 printf와 달리 Zig은 리터럴 문자열과 변수 리스트를 컴파일 타임에 처리 가능
실행 파일 디버깅
- 디버거 사용은 통합 디버거가 있는 IDE(Eclipse, IntelliJ IDEA) 또는 통합 개발 킷(w64devkit) 외에는 간단하지 않음
- 심볼 통합은 코드를 비대화하고 Debug 모드 컴파일을 요구하여 효율성이 현저히 낮은 실행 코드 생성
-
Zig은 이러한 문제를 피하기 위한 편리한 해결책 제공
@breakpoint 내장 함수
-
소스 코드에
@breakpoint();를 삽입하여 디버거에서 실행 시 해당 지점에서 프로그램 중지
-
심볼 없이 최적화된 Zig 코드를 디버깅할 수 있는 유용한 기능
-
@breakpoint(); 직전에 std.debug.print를 사용하여 추적할 변수를 출력하면 해당 순간의 변수 값 확인 가능
- debug_example.zig 예제에서
set 함수 내부에 grid와 변수들을 출력하는 코드와 @breakpoint(); 삽입
- 빌드 명령:
zig build-exe debug_example.zig
- gdb 같은 디버거로 debug_example.exe 호출 후
r 명령으로 프로그램 실행
-
c 명령으로 계속 진행하며 grid 내용과 변수 추적
- Enter 반복 입력으로 계속 진행하면, grid의 값들이 example.zig의 테스트 블록과 일치함을 확인 가능
Zig의 저수준 프로그래밍
행렬 표현
-
십진수 숫자는 표준
u8 정수로 행렬에 저장
- 입력 grid는 문자열 형식이지만 ASCII 문자는 내부적으로 u8 정수로 변환
-
숫자 저장은 81개 위치의 배열
grid에 선형으로 한 줄씩 구성: var grid = [_]u8{0} ** 81;
- grid 정확성 검증을 위해 각 줄과 열로 요소 접근 필요
-
9개 위치의 포인터 배열 생성, 각 포인터는 각 줄의 시작을 가리킴
-
레이블 브레이크를 사용하여 코드 블록에서 값 반환:
break :fill9x9 m;로 matrix를 m으로 초기화
- 요소 접근 표기:
element = matrix[i][j]
십진수 숫자를 비트로 표현
-
정수 십진수 숫자
i를 정수 code로 대체하는 핵심 개념
-
i ∈ [1,9] → code = 2ⁱ⁻¹
-
i = 0 → code = 0
-
code의 유일한 비트가 1로 설정되는 위치는 i-1 (i가 1~9 사이일 때), 그렇지 않으면 모든 비트가 0
- 각 숫자에 대한 code 값 표 제공 (1→1, 2→2, 3→4, ..., 9→256)
Zig에서 code 계산
-
c가 0이 아닐 때만 왼쪽 시프트 연산자로 code 값 계산: code = @as(u9,1) << (c-1);
-
Zig에서 상수는 연산이 컴파일되고 결과가 변수에 할당되려면 적절한 크기가 필요
-
code는 u9 타입으로 선언 (최대값 256은 최소 9비트 필요)
-
Zig은 임의 비트 크기 변수 보유 가능
-
내장 함수
@as로 1 상수를 u9 타입으로 캐스팅
비트 필드를 사용한 grid 표현
줄별 비트 필드 grid
-
배열
lines는 각 줄을 9비트 정수로 표현하여 전체 grid를 미러링: var lines = [_]u9{0} ** 9;
-
줄
i로 배열 접근 시, 특정 숫자가 해당 줄에 이미 있는지 비트 AND 연산(&)으로 확인: lines[i] & code
- 연산 결과가 0이면 숫자가 줄 i에 아직 없음, 그렇지 않으면 중복
열별 비트 필드 grid
-
배열
columns는 각 열을 9비트 정수로 표현하여 전체 grid를 미러링: var columns = [_]u9{0} ** 9;
-
열
j로 배열 접근 시, 특정 숫자가 해당 열에 이미 있는지 비트 AND 연산으로 확인: columns[j] & code
- 연산 결과가 0이면 숫자가 열 j에 아직 없음, 그렇지 않으면 중복
스도쿠 규칙
- 빈 스도쿠 grid에 새 숫자를 삽입할 때, 새 요소를 포함하는 전체 줄, 열, 셀에 이미 존재하지 않아야 함
-
셀은 굵은 선으로 구분된 9개의 3x3 grid 각각
- 9x9 grid의 각 특정 요소는 해당 요소를 포함하는 고유한 줄, 열, 셀을 가짐
- 예제 grid에서 첫 번째 셀은 3, 5, 6, 8, 9를 포함하며, 1, 2, 4, 7이 누락
- 배열
lines와 columns는 줄과 열의 중복 검사를 처리
-
셀의 중복 검사를 위해 새 배열 필요
셀별 비트 필드 grid
-
배열
cells는 각 셀을 9비트 정수로 표현하여 전체 grid를 미러링: var cells = [_]u9{0} ** 9;
-
cells를 3x3 행렬로 접근하면 더 쉬움
- 9x9 행렬에서 수행한 것과 유사하게 배열
cell 채우기
- 원래 9x9 grid의 요소 줄과 열에서
cell 행렬의 줄과 열 결정 필요
- 정수 나눗셈은 매우 느리므로, 배열
cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 };를 사용하여 나눗셈 결과 제공
-
9x9 grid의 요소 줄
i와 열 j로 행렬 접근 시, 특정 숫자가 요소의 셀에 이미 있는지 비트 AND 연산으로 확인: cell[cindx[i]][cindx[j]] & code
- 연산 결과가 0이면 숫자가 셀에 아직 없음, 그렇지 않으면 중복
요소 중복 테스트
-
동일한 줄, 열, 셀의 모든 이전 요소를 비트 OR(
|)로 결합한 후, 요소의 code와 비트 AND 수행하여 요소 중복 검증 완료
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {
unreachable;
}
- 결과가 0이면 요소가 아직 줄, 열, 셀에 존재하지 않음
- 결과가 0이 아니면 프로그램은
unreachable 명령 실행으로 중지
-
Zig에서 실행 오류를 명시적으로 나타내는 가장 간단한 방법
- 실제 코드는 오류 발생 위치의 세부 정보도 출력
- 예: 입력 문자열의 첫 '8' 바로 다음 '0'을 '5'로 교체하면, 열 1의 줄 3에 이미 5가 있어 오류 발생
데이터 구조 업데이트
-
set 함수에서 이중 for 루프가 줄별로 상호 작용하여 입력 문자열 s의 각 새 요소를 grid에 복사
- 변수
k는 문자열 s의 새 입력 문자 인덱스 유지
-
문자는
'0'을 빼서 u4(변수 c)로 변환
- grid에 삽입할 새 요소가 0이 아니면(
c != 0), 왼쪽 시프트 명령으로 계산된 code를 각 미러 grid에 복사
- 해당 미러 grid와 비트 OR(
|=) 수행:
lines[i] |= code;
columns[j] |= code;
cell[cindx[i]][cindx[j]] |= code;
-
c 값이 1~9 사이인지 명시적으로 테스트할 필요 없음 - 시프트 연산 실행 시 오버플로가 발생하기 때문
- 예: 입력 문자열의 첫 '8' 바로 다음 '0'을 ':'로 교체하면 실행 오류 발생
- 같은 '0'을 '/'로 교체해도 유사한 실행 오류 발생
- 프로그램은 값이 1~9 사이, 즉 입력 grid가 십진수만 포함할 때만 작동
- 웹의 많은 스도쿠 grid는 '0'을 '.'로 표현하므로,
set 함수에 if (s[k] == '.') c = 0; 라인 존재
-
이는
c 값이 0이므로 시프트 연산을 편리하게 우회
프로토타이핑과 견고성
- 위 두 섹션의 강제 오류는 Zig의 중요한 기능 시연
- 하나는 Zig의 견고성 - 시프트 연산의 경우 잘못된 동작이 허용되지 않으며 실행 시간에 포착됨
- 모든 노력이 효율성을 향한 것처럼 보이지만, 성능이 견고성과 교환된 전형적인 사례
- C에서는 시프트 연산이 비트를 잃어도 프로그래머의 문제이며, 이는 특정 어셈블러 명령의 더 나은 성능으로 변환됨
- 또 다른 기능은 테스트 블록을 프로토타이핑에 사용할 가능성
- 응용 가능성은 무수히 많으며, 보여진 응용은 오류 발생 시 특정 상황을 디버깅하는 것뿐
-
이러한 기능만으로도 프로그래밍 언어에서 매우 드문 놀라운 능력 제공, 특히 컴파일된 프로그래밍 언어에서
결론
- Zig는 C 호환성, 크로스 컴파일, 간단한 설치라는 세 가지 핵심 요소로 구성
- 이러한 특성은 시스템 프로그래밍 언어의 새로운 표준으로 자리 잡을 가능성을 보여줌
-
인터프리터 언어에서만 발견되던 많은 장점이 더 나은 성능을 제공하기 위해 점차 컴파일 언어로 이동
-
Zig은 컴파일 타임 실행 개념으로 인터프리터 언어와의 유사성이 매우 두드러짐
- 이는 Zig을 특별히 다르고 강력하게 만드는 동시에 이해하기 어렵게 만들기도 함