- Trình biên dịch Zig cung cấp sẵn khả năng biên dịch mã C và biên dịch chéo là ngôn ngữ gây kinh ngạc nhất mà tác giả từng trải nghiệm trong 45 năm làm nghề
- Với những tính năng độc đáo như thực thi tại thời điểm biên dịch, biến có kích thước bit tùy ý, môi trường khối kiểm thử, Zig không chỉ là bản thay thế đơn thuần cho C/C++ mà còn mang đến một cách lập trình hoàn toàn mới
- Nhờ cú pháp ngắn gọn và rõ ràng như khai báo biến bằng suy luận kiểu, struct ẩn danh, label break, người học có thể tiếp cận rất nhanh
- Hỗ trợ gỡ lỗi mã tối ưu hóa bằng kiểm thử mô-đun độc lập thông qua khối kiểm thử và hàm dựng sẵn @breakpoint
- Hỗ trợ lập trình mức thấp bằng bit field và phép toán bit, qua đó đồng thời đạt được hiệu quả lẫn độ vững chắc, đồng thời tích hợp ưu điểm của ngôn ngữ thông dịch vào ngôn ngữ biên dịch
Lời mở đầu
- Trong 45 năm kinh nghiệm, chưa từng có ngôn ngữ nào gây ấn tượng như Zig
- Zig không chỉ là một ngôn ngữ mới, mà là công cụ thay đổi tận gốc cách lập trình
- Chỉ xem Zig ở mức thay thế C hay C++ là đánh giá thấp nghiêm trọng
- Mục đích của bài viết này là giới thiệu những tính năng đơn giản nhưng hấp dẫn của Zig và giúp lập trình viên có thể bắt đầu nhanh chóng
- Trong công nghiệp còn có nhiều tính năng khác ảnh hưởng đến mức độ chấp nhận Zig
Trình biên dịch Zig
- Zig mặc định cung cấp khả năng biên dịch mã C và biên dịch chéo mà không cần cấu hình riêng, tạo ảnh hưởng lớn trong công nghiệp
- Cài đặt bằng cách tải trình biên dịch tương ứng với bộ xử lý/HĐH từ trang tải xuống của Ziglang, giải nén rồi chép vào thư mục mong muốn
- Trên Windows 10, có thể chép tệp zip x86_64 vào
Program Files, đổi tên thư mục gốc thành zig-windows-x86_64 để khi nâng cấp phiên bản không cần sửa biến môi trường Path
- Sau khi thêm đường dẫn thư mục gốc vào biến môi trường Path, có thể dùng trình biên dịch ở chế độ CLI
- Để build chương trình
Hello World!, nên tham khảo mục Getting Started trên trang chính thức
Khái niệm và lệnh chính
Khai báo biến
- Khai báo biến gồm phần đầu là quyền truy cập (
pub hoặc lược bỏ), var/const, tên biến; phần hai là khai báo kiểu; phần ba là khởi tạo
- Chỉ phần đầu và phần ba là bắt buộc, còn kiểu có thể được suy ra từ giá trị khởi tạo
- Ví dụ:
var sum : usize = 0;
- Biến khai báo không có
pub chỉ truy cập được trong mô-đun (tương tự biến static trong C)
- Không khuyến nghị khai báo biến
pub; nên giảm thiểu hàm pub để hạ độ kết dính giữa các thành phần và tăng tính gắn kết nội bộ
Struct, struct ẩn danh, khối kiểm thử
- Literal struct ẩn danh được bao bởi
.{ và } được dùng để khởi tạo phần tử của struct khác hoặc tạo struct mới đã khởi tạo phần tử
.{ } là literal struct ẩn danh rỗng
- Dạng
struct { } là khai báo struct
- Khối kiểm thử cho phép biên dịch và chạy kiểm thử mà không cần file thực thi
Bit field
- Bit field được khai báo trong
packed struct dưới dạng trường có kiểu với kích thước cụ thể
- Con trỏ có thể trỏ tới một bit field cụ thể
Vòng lặp for
- Cú pháp Zig rõ ràng hơn C, nhưng dùng khoảng mở
[0..9) thay vì [0..8]
- Việc khai báo kiểu, khởi tạo, kiểm tra và tăng biến lặp
i đều được xử lý tự động
Mảng
[_] định nghĩa một mảng chưa biết kích thước, sau đó là kiểu phần tử và phần khởi tạo
- Ví dụ:
var grid = [_]u8{0} ** 81; khởi tạo 81 phần tử u8 bằng 0
- Kích thước mảng được suy ra từ đối số lặp trong khởi tạo
- Trong môi trường kiểm thử, có thể duyệt các phần tử mảng và tính tổng
- Biến được khai báo giữa dấu
| của vòng for mặc định được xem là cùng kiểu với phần tử mảng
usize là số nguyên không dấu tự nhiên của nền tảng (u64 trên 64-bit, u32 trên 32-bit)
Con trỏ nhiều phần tử
- Nếu con trỏ mảng muốn dùng phép toán số học trên con trỏ thì phải khai báo tường minh là con trỏ nhiều phần tử như
[*]const i32
- Dù mảng là
const, con trỏ vẫn có thể được khai báo là var
Giải tham chiếu con trỏ
- Con trỏ được gán địa chỉ của một vị trí mảng riêng lẻ thì không thể cập nhật bằng số học con trỏ
- Giải tham chiếu con trỏ dùng
ptr.*
Label break
- Có thể thực hiện nhiều công việc khác nhau tại thời điểm biên dịch, như khởi tạo mảng
- Label break đặt
: sau tên khối, rồi dùng break để trả về giá trị từ khối
0.. là phạm vi vô hạn bắt đầu từ 0
- Trong vòng
for, các biến được tự động khởi tạo và tăng; vòng lặp dừng sau khi xử lý vị trí cuối của mảng
- Mảng có thể không cần khởi tạo tường minh bằng
undefined
Hàm trong Zig
- Hàm được khai báo bằng
fn và mặc định là static (chỉ dùng trong file)
- Nếu khai báo
pub fn thì có thể được import từ file khác
- Hàm có thể được "inlined"
- Con trỏ hàm có
const ở trước và prototype hàm ở sau
Lập trình hướng đối tượng trong Zig
- Struct có thể chứa hàm
- Trong ví dụ stack, có thể lưu tối đa 81 phần tử (kiểu
StkNode)
- Zig không có toán tử
++ và --; thay vào đó dùng += và -=
- Con trỏ stack là một số nguyên dùng làm chỉ mục của mảng
stk
- Con trỏ
self không được truyền tường minh như tham số; nó được ngầm hiểu gián tiếp là con trỏ tới thể hiện stack đang gọi hàm
- Khi gọi như
stack.pop(), self là con trỏ tới stack (tương tự this trong Java/C++)
- Hàm
init() là constructor của stack
- Các hàm
pop và push được "inlined"
Build và chạy chương trình Zig
Build file thực thi
- Để tạo file thực thi cần có hàm
main biểu thị điểm vào chương trình
- Chương trình đơn giản có thể đặt hàm
main trong cùng file
- Để gỡ lỗi mô-đun độc lập, có thể chèn hàm
main ở cuối file rồi comment lại sau khi debug xong
- Lệnh biên dịch:
zig build-exe -O ReleaseFast program.zig
Chạy khối kiểm thử của mô-đun
- Đây là một trong những tính năng xuất sắc nhất của Zig, dùng cho kiểm thử và tạo prototype
- Khối kiểm thử bắt đầu bằng
test "message" { và kết thúc bằng }
message là chuỗi được hiển thị khi chạy kiểm thử
- Khối kiểm thử chạy độc lập với file thực thi; file thực thi cuối cùng sẽ không chạy các kiểm thử này
- Lệnh kiểm thử:
zig test module.zig
- Khối kiểm thử trong
example.zig kiểm tra các hàm set và print; set nhận chuỗi thập phân làm tham số, còn print in tiêu đề Input Grid rồi in ra grid
Xuất dữ liệu trong Zig
- Câu lệnh
std.debug.print gọi hàm print trong debug.zig của thư viện chuẩn Zig std
- Tham số đầu tiên là chuỗi định dạng, tham số thứ hai là struct ẩn danh chứa danh sách biến cần hiển thị
- Nếu không có định dạng thì struct sẽ rỗng
- Mặc định hiển thị ra
stderr
- Khác với
printf của C, Zig có thể xử lý chuỗi literal và danh sách biến tại thời điểm biên dịch
Gỡ lỗi file thực thi
- Việc dùng debugger không đơn giản nếu không có IDE tích hợp debugger như Eclipse, IntelliJ IDEA hoặc bộ công cụ phát triển tích hợp như w64devkit
- Tích hợp symbol làm mã phình to và yêu cầu biên dịch ở chế độ Debug, dẫn đến mã thực thi kém hiệu quả đáng kể
- Zig cung cấp một cách giải quyết tiện lợi để tránh các vấn đề đó
Hàm dựng sẵn @breakpoint
- Chèn
@breakpoint(); vào mã nguồn để khi chạy trong debugger, chương trình sẽ dừng tại đúng điểm đó
- Đây là tính năng hữu ích để gỡ lỗi mã Zig đã tối ưu hóa mà không cần symbol
- Ngay trước
@breakpoint();, có thể dùng std.debug.print để in các biến cần theo dõi và xem giá trị của chúng tại thời điểm đó
- Trong ví dụ
debug_example.zig, tác giả chèn mã in grid cùng các biến bên trong hàm set và thêm @breakpoint();
- Lệnh build:
zig build-exe debug_example.zig
- Gọi
debug_example.exe bằng debugger như gdb rồi dùng lệnh r để chạy chương trình
- Dùng lệnh
c để tiếp tục và theo dõi nội dung grid cùng các biến
- Nếu tiếp tục nhấn Enter để chạy tiếp, có thể xác nhận rằng các giá trị trong
grid khớp với khối kiểm thử của example.zig
Lập trình mức thấp trong Zig
Biểu diễn ma trận
- Các chữ số thập phân được lưu trong ma trận bằng số nguyên chuẩn
u8
grid đầu vào ở dạng chuỗi, nhưng ký tự ASCII sẽ được chuyển nội bộ thành số nguyên u8
- Việc lưu trữ số được tổ chức tuyến tính theo từng dòng trong mảng
grid gồm 81 vị trí: var grid = [_]u8{0} ** 81;
- Để kiểm tra tính đúng đắn của
grid, cần truy cập các phần tử theo từng dòng và cột
- Tạo một mảng gồm 9 con trỏ, mỗi con trỏ trỏ tới đầu của một dòng
- Dùng label break để trả về giá trị từ một khối mã:
break :fill9x9 m; để khởi tạo matrix bằng m
- Cú pháp truy cập phần tử:
element = matrix[i][j]
Biểu diễn chữ số thập phân bằng bit
- Khái niệm cốt lõi là thay chữ số thập phân nguyên
i bằng số nguyên code
i ∈ [1,9] → code = 2ⁱ⁻¹
i = 0 → code = 0
- Vị trí bit duy nhất được đặt thành
1 trong code là i-1 (khi i nằm trong khoảng 1~9), nếu không thì mọi bit đều bằng 0
- Bài viết cung cấp bảng giá trị code cho từng số (1→1, 2→2, 3→4, ..., 9→256)
Tính code trong Zig
- Chỉ khi
c khác 0 mới tính giá trị code bằng toán tử dịch trái: code = @as(u9,1) << (c-1);
- Trong Zig, hằng số cần có kích thước phù hợp để phép toán được biên dịch và kết quả được gán cho biến
code được khai báo là kiểu u9 (vì giá trị lớn nhất 256 cần ít nhất 9 bit)
- Zig cho phép dùng biến có kích thước bit tùy ý
- Dùng hàm dựng sẵn
@as để ép kiểu hằng 1 sang u9
Biểu diễn grid bằng bit field
Grid bit field theo dòng
- Mảng
lines phản chiếu toàn bộ grid, biểu diễn mỗi dòng bằng một số nguyên 9 bit: var lines = [_]u9{0} ** 9;
- Khi truy cập dòng
i, có thể kiểm tra một chữ số cụ thể đã tồn tại trong dòng đó chưa bằng phép AND bit (&): lines[i] & code
- Nếu kết quả là 0 thì số đó chưa có trong dòng
i, ngược lại là bị trùng
Grid bit field theo cột
- Mảng
columns phản chiếu toàn bộ grid, biểu diễn mỗi cột bằng một số nguyên 9 bit: var columns = [_]u9{0} ** 9;
- Khi truy cập cột
j, có thể kiểm tra một chữ số cụ thể đã tồn tại trong cột đó chưa bằng phép AND bit: columns[j] & code
- Nếu kết quả là 0 thì số đó chưa có trong cột
j, ngược lại là bị trùng
Quy tắc Sudoku
- Khi chèn một số mới vào
grid Sudoku rỗng, nó không được tồn tại sẵn trong toàn bộ dòng, cột và ô chứa phần tử mới đó
- Ô ở đây là một trong 9 vùng lưới 3x3 được ngăn bởi các đường đậm
- Mỗi phần tử cụ thể trong lưới 9x9 thuộc về đúng một dòng, một cột và một ô
- Trong lưới ví dụ, ô đầu tiên chứa 3, 5, 6, 8, 9 và còn thiếu 1, 2, 4, 7
- Các mảng
lines và columns xử lý việc kiểm tra trùng lặp theo dòng và cột
- Cần thêm một mảng mới để kiểm tra trùng lặp theo ô
Grid bit field theo ô
- Mảng
cells phản chiếu toàn bộ grid, biểu diễn mỗi ô bằng một số nguyên 9 bit: var cells = [_]u9{0} ** 9;
- Sẽ dễ hơn nếu truy cập
cells như một ma trận 3x3
- Điền mảng
cell tương tự như cách đã làm với ma trận 9x9
- Cần xác định dòng và cột của ma trận
cell từ dòng và cột của phần tử trong grid 9x9 gốc
- Vì phép chia số nguyên rất chậm, dùng mảng
cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; để cung cấp kết quả chia
- Khi truy cập ma trận bằng dòng
i và cột j của phần tử trong lưới 9x9, có thể kiểm tra một chữ số cụ thể đã tồn tại trong ô của phần tử đó chưa bằng phép AND bit: cell[cindx[i]][cindx[j]] & code
- Nếu kết quả là 0 thì số đó chưa có trong ô, ngược lại là bị trùng
Kiểm tra trùng phần tử
- Có thể hoàn tất việc kiểm tra trùng lặp bằng cách OR bit (
|) tất cả các phần tử trước đó trong cùng dòng, cột và ô, rồi AND bit với code của phần tử
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {
unreachable;
}
- Nếu kết quả là 0, phần tử đó chưa tồn tại trong dòng, cột hoặc ô
- Nếu kết quả khác 0, chương trình sẽ dừng bằng lệnh
unreachable
- Đây là cách đơn giản nhất trong Zig để biểu thị tường minh lỗi thực thi
- Mã thực tế còn in ra chi tiết vị trí phát sinh lỗi
- Ví dụ: nếu thay ký tự
0 ngay sau ký tự 8 đầu tiên trong chuỗi đầu vào bằng 5, sẽ phát sinh lỗi vì ở dòng 3 cột 1 đã có số 5
Cập nhật cấu trúc dữ liệu
- Trong hàm
set, vòng for lồng nhau tương tác theo từng dòng để chép từng phần tử mới từ chuỗi đầu vào s vào grid
- Biến
k giữ chỉ số của ký tự đầu vào mới trong chuỗi s
- Ký tự được chuyển thành
u4 (biến c) bằng cách trừ '0'
- Nếu phần tử mới chèn vào
grid khác 0 (c != 0), code được tính bằng lệnh dịch trái sẽ được chép vào từng lưới phản chiếu
- Thực hiện OR bit với lưới phản chiếu tương ứng (
|=):
lines[i] |= code;
columns[j] |= code;
cell[cindx[i]][cindx[j]] |= code;
- Không cần kiểm tra tường minh xem
c có nằm trong khoảng 1~9 hay không, vì phép dịch sẽ gây tràn khi thực thi nếu giá trị không hợp lệ
- Ví dụ: nếu thay ký tự
0 ngay sau ký tự 8 đầu tiên trong chuỗi đầu vào bằng :, sẽ xảy ra lỗi thực thi
- Thay cùng ký tự
0 đó bằng / cũng gây lỗi thực thi tương tự
- Chương trình chỉ hoạt động khi giá trị nằm trong khoảng 1~9, tức
grid đầu vào chỉ chứa chữ số thập phân
- Nhiều lưới Sudoku trên web dùng
. thay cho 0, nên trong hàm set có dòng if (s[k] == '.') c = 0;
- Cách này giúp bỏ qua thuận tiện phép dịch bit vì
c khi đó bằng 0
Tạo prototype và độ vững chắc
- Hai phần trước minh họa các lỗi cưỡng bức như một tính năng quan trọng của Zig
- Một mặt là độ vững chắc của Zig — với phép dịch bit, hành vi sai không được phép tồn tại và sẽ bị bắt ở thời gian chạy
- Dù mọi nỗ lực có vẻ đều hướng tới hiệu quả, đây lại là trường hợp điển hình khi hiệu năng được đánh đổi với độ vững chắc
- Trong C, nếu phép dịch làm mất bit thì đó là vấn đề của lập trình viên, đổi lại có hiệu năng tốt hơn từ một số lệnh assembler cụ thể
- Tính năng còn lại là khả năng dùng khối kiểm thử để tạo prototype
- Khả năng ứng dụng là vô số, và ví dụ được trình bày chỉ là gỡ lỗi một tình huống cụ thể khi phát sinh lỗi
- Chỉ riêng những tính năng này cũng đã mang lại năng lực đáng kinh ngạc vốn rất hiếm thấy ở ngôn ngữ lập trình, đặc biệt là ngôn ngữ biên dịch
Kết luận
- Zig được cấu thành từ ba yếu tố cốt lõi: tương thích C, biên dịch chéo, và cài đặt đơn giản
- Những đặc tính này cho thấy tiềm năng của Zig trong việc trở thành chuẩn mực mới cho ngôn ngữ lập trình hệ thống
- Nhiều ưu điểm vốn chỉ có ở ngôn ngữ thông dịch đang dần chuyển sang ngôn ngữ biên dịch để mang lại hiệu năng tốt hơn
- Zig đặc biệt nổi bật về sự tương đồng với ngôn ngữ thông dịch nhờ khái niệm thực thi tại thời điểm biên dịch
- Điều đó vừa khiến Zig trở nên khác biệt và mạnh mẽ, vừa khiến nó khó nắm bắt hơn
1 bình luận
Ý kiến Hacker News
Bài viết mở đầu bằng việc cho rằng “Zig không chỉ là một ngôn ngữ đơn giản mà là một cách lập trình hoàn toàn mới”, nhưng thực tế lại hầu như không đề cập đến các tính năng thực sự đặc trưng của Zig
Suy luận kiểu, struct ẩn danh, labeled break... đều đã tồn tại từ rất lâu trong các ngôn ngữ khác
Thứ thực sự độc đáo là comptime, nhưng phần này lại không được nhắc đến chút nào
Dù không phải là khái niệm hoàn toàn mới như macro của Lisp, cách Zig dùng nó thay cho generic khá thú vị
Tuy vậy, lập luận của bài viết tạo cảm giác cường điệu hóa khá nhiều
Rust cho phép biểu đạt rõ ràng thời điểm mã được thực thi, và thiết kế kiểu giống một query engine để duyệt toàn bộ không gian mã nguồn rất ấn tượng
Xem tài liệu D
Nếu là const-expression thì nó sẽ tự động được thực thi
Chúng khác nhau như Java và Scala vậy
Zig gọn gàng hơn template của C++, nhưng tạo cảm giác là một lựa chọn thay thế thực dụng hơn là một cuộc cách mạng
Cá nhân tôi không hiểu được sự cuồng nhiệt quá mức dành cho nó, giống như thời Rust trước đây
Tôi đã đọc hết tài liệu Zig mà vẫn không thấy điều gì đáng kinh ngạc, nên khá bối rối
Vấn đề lớn nhất của Zig là không thể gắn dữ liệu vào error
Error chỉ được truyền qua một kênh phụ, khiến việc debug khó khăn, và rốt cuộc các lập trình viên thường bỏ qua dữ liệu lỗi
Xem issue liên quan
Chỉ với những mã đơn giản như AccessDenied thì rất khó biết nguyên nhân là gì
Trên thực tế, ngay cả khi dùng một đối tượng
Errorphức tạp thì nhiều khi vẫn cần một kênh chẩn đoán riêngVì chi phí hiệu năng hoặc vấn đề trạng thái hệ thống, trong nhiều trường hợp xử lý bằng delayed binding sẽ an toàn hơn
Zig theo đuổi triết lý ưu tiên độ chính xác và tính quyết định kiểu này
Xem issue liên quan
Nhưng điều thực sự cần là structured logging và khả năng theo dõi ngữ cảnh dựa trên call stack
std.zonthường được nêu như một ví dụ tốt, và trong cộng đồng cũng đang có xu hướng tập hợp nhiều mẫu xử lý lỗi khác nhau để phản ánh vào tiêu chuẩnNó có thể ngăn những lập trình viên lười biếng bừa bãi nhồi dữ liệu vào mọi thứ
Tôi đồng ý với nhận định rằng “cách phát triển Zig tự thân nó là một cách phát triển ngôn ngữ mới”
Quá trình tiến hóa chậm rãi với việc cân nhắc kỹ tính năng và loại bỏ những thứ không cần thiết khá ấn tượng
Tôi muốn nghe cụ thể hơn đâu là điểm thật sự độc đáo của Zig
Tôi thích việc có thể cài Zig bằng PyPI
Chỉ cần cài gói ziglang bằng
pip install ziglanglà có thể dùng ngayCũng có thể dùng
uvxđể build mã CThật đáng tiếc khi những tính năng vốn đã tồn tại từ lâu trong các ngôn ngữ như Ada, Object Pascal, Modula-2 lại được đóng gói thành “đổi mới” của Zig
Thật thú vị khi các ý tưởng từ 40 năm trước lại trông mới mẻ chỉ vì được bọc lại bằng cú pháp kiểu C
Phần mở đầu của bài viết khá ổn, nhưng sau đó chỉ dừng ở việc liệt kê các tính năng của Zig
Cú pháp trực quan và luồng điều khiển tường minh của Zig (
deferchẳng hạn) rất hấp dẫnNhờ comptime nên cũng không cần học thêm cú pháp macro riêng biệt
Mọi thành phần đều ăn khớp tự nhiên đến mức dù mới dùng cũng có cảm giác như một công cụ đã gắn bó từ lâu
Cú pháp
for (0..9)của Zig khá trực quan, nhưng vì là khoảng mở nên đôi khi dễ gây nhầm lẫnGiống như
range(0, 9)của Python, rất dễ quên liệu giá trị cuối có được bao gồm hay không0..9và0..=9nên minh bạch hơnKích thước khoảng có thể tính đơn giản bằng hiệu số, và việc duyệt ngược cũng trở nên dễ hơn
0..<5(mở) và0...5(đóng)Tôi không thích quy tắc định danh của Zig
Việc trộn lẫn snake_case và camelCase tạo cảm giác hơi kỳ
Dù vậy, hệ thống build, allocator bộ nhớ và trải nghiệm biên dịch của nó đều rất xuất sắc
Tôi chủ yếu dùng Rust, nhưng vẫn luôn tò mò về Zig
Quy ước tiền tố của thư viện C cũng phiền tương tự
Sức hút của Zig không nằm ở một tính năng đơn lẻ nào, mà ở sự tích lũy của các quyết định thực dụng
Những lựa chọn ban đầu trông có vẻ cấp tiến, nhưng càng hiểu sâu lại càng thấy hợp lý
Zig là một ngôn ngữ tưởng thưởng cho những lập trình viên hiếu kỳ
Một trong những lý do Zig được đánh giá cao là vì nó thừa nhận thực tế của mã hệ thống cấp thấp
Nhiều ngôn ngữ né tránh phần này vì lý do thẩm mỹ, nhưng Zig thì không
Xem tài liệu page_allocator