Từ Rust sang Zig: Trải nghiệm thực tế của một lập trình viên hệ thống

Công nghệ19 tháng 9, 2026·7 phút đọc

Một lập trình viên với 7 năm kinh nghiệm Rust chia sẻ cảm nhận khi chuyển sang Zig để viết lại thư viện JSONPath. Bài viết phân tích sự khác biệt về IDE, cấu trúc thư mục, kiểm thử và đặc biệt là cách quản lý bộ nhớ thủ công của Zig so với mô hình sở hữu của Rust.

Từ Rust sang Zig: Trải nghiệm thực tế của một lập trình viên hệ thống

Một lập trình viên với 7 năm gắn bó cùng Rust đã thử sức với Zig bằng cách viết lại thư viện JSONPath từ đầu. Kết quả là một góc nhìn thú vị về sự khác biệt giữa hai ngôn ngữ hệ thống, đặc biệt ở cách quản lý bộ nhớ và triết lý thiết kế.

Sau 7 năm làm việc chủ yếu với các dự án mã nguồn mở bằng Rust, tác giả quyết định thử sức với Zig — ngôn ngữ đang được xem như ứng viên kế thừa C. Để phép so sánh công bằng, anh chọn viết lại thư viện JSONPath (đặc tả RFC 9535) mà trước đó đã có bản Rust là jsonpath-rust.

Hỗ trợ IDE: Cú sốc đầu tiên

Điều khiến tác giả bất ngờ nhất không phải cú pháp, mà là mức hỗ trợ IDE gần như bằng không. So với RustRover hay các IDE của JetBrains, Zig chỉ cung cấp tô sáng cú pháp và gợi ý cơ bản.

Điều này buộc tác giả quay về làm việc qua dòng lệnh — và hóa ra đó lại là phần thú vị nhất. Công cụ build.zig xử lý mọi thứ rất gọn gàng:

zig build test                                              # chạy tất cả test
zig build test -Dfilter="filter match function basic"       # chạy một test
zig build test -Ddebug-query=true                           # tất cả test kèm debug
zig build compliance                                        # bộ kiểm thử tuân thủ
zig build check                                             # unit test + compliance

Chính trải nghiệm này đã khởi động một thay đổi lớn hơn: tác giả chuyển hẳn khỏi IDE truyền thống sang bộ công cụ helix + alacritty + zellij.

Cấu trúc phẳng: Triết lý thiết kế khác biệt

Với Rust, tác giả thường dành nhiều thời gian cân nhắc giữa kích thước file và độ sâu thư mục. Zig thì không khuyến khích điều đó. Bạn vẫn có thể lồng thư mục, nhưng sẽ gặp chút trở ngại khi import — và câu hỏi thực sự là: chia nhỏ để làm gì?

Sự khác biệt thể hiện rõ qua cấu trúc thư mục thực tế:

Rust (src/):

src/
├── lib.rs
├── parser.rs
├── parser/
│   ├── errors.rs
│   ├── macros.rs
│   ├── model.rs
│   ├── tests.rs
│   └── grammar/
│       └── json_path_9535.pest
├── query.rs
└── query/
    ├── atom.rs
    ├── comparable.rs
    ├── comparison.rs
    ├── filter.rs
    ├── jp_query.rs
    ├── queryable.rs
    ├── segment.rs
    ├── selector.rs
    ├── state.rs
    ├── test.rs
    └── test_function.rs

Zig (src/):

src/
├── root.zig
├── parser.zig
├── model.zig
├── model_query.zig
└── query.zig

Nếu bạn không thể cưỡng lại việc tạo thư mục ngay từ ngày đầu, có lẽ dự án nhỏ hơn bạn tưởng.

Tác giả thừa nhận cách này khó mở rộng cho dự án lớn, nhưng ngưỡng cần phân cấp thư mục ở Zig cao hơn nhiều so với dự đoán.

Kiểm thử: Rust vẫn nhỉnh hơn

Rust có hai cách kiểm thử quen thuộc: unit test nội tuyến trong cùng file và integration test trong thư mục tests riêng. Zig về lý thuyết cũng tương tự, nhưng vấn đề nằm ở độ dài dòng.

Với cấu trúc phẳng đã chọn, tác giả buộc phải viết test nội tuyến trong từng file model — sau đó cấu hình tường minh trong build.zig. Khi đã thiết lập xong thì chạy tốt, nhưng tổng thể vẫn mất công hơn Rust. Điểm mấu chốt: phần lớn trở ngại đến từ quản lý bộ nhớ thủ công của Zig, không phải hạ tầng kiểm thử.

Không có mô hình hàm: Sự chuyển dịch lớn

Rust tuy là ngôn ngữ mệnh lệnh nhưng đậm chất hàm: iterator chi phí bằng không, đánh giá lười, kiểu ADT, so khớp mẫu, closure, trait. Tác giả đã quen với Haskell và Erlang nên thư viện Rust của anh dùng nhiều mẫu hàm.

Điểm tương đồng giữa hai ngôn ngữ là kiểu tổng (sum types). Nhưng khi sang Zig, mọi thứ đổi hướng sang đột biến tại chỗ (in-place mutation) và mẫu mệnh lệnh.

Ví dụ, kiểu Query trong Rust:

pub trait Query {
    fn process(&self, state: State) -> State;
}

Còn Zig dùng duck typing với kiểm tra lúc biên dịch:

pub fn query(node: anytype, iteration: *JsonPathIter) !void {
    const T = switch (@typeInfo(@TypeOf(node))) {
        .pointer => |p| p.child,
        else => @TypeOf(node),
    };
    if (!@hasDecl(T, "query")) {
        return; // không có trait lúc biên dịch, chỉ kiểm tra phương thức tồn tại
    }
    try node.query(iteration);
}

Sự khác biệt cốt lõi nằm ở monad bất biến của Rust so với đột biến của Zig, chủ yếu vì Zig buộc lập trình viên làm việc trực tiếp với allocator.

Quản lý bộ nhớ: Ba lỗi kinh điển

Đây là phần thú vị nhất của bài viết. Tác giả phân tích ba lỗi điển hình mà Rust ngăn chặn hoàn toàn, nhưng Zig không.

1. Rò rỉ bộ nhớ khi khởi tạo iterator thất bại

var iter = JsonPathIter.init(a);
// LỖI: nếu append sau đó thất bại, iter bị rò rỉ
try iter.ensureTotalCapacity(10);
return iter;

Bắt lỗi bằng FailingAllocator{ .fail_index = 1 }. Cách sửa: thêm errdefer iter.deinit(); ngay sau khởi tạo. Rust loại bỏ hoàn toàn vấn đề này vì Drop::drop luôn chạy khi thoát scope.

2. Hủy bộ nhớ hai lần khi chuyển quyền sở hữu

fn cacheAndLog(json: *Value, qstr: []const u8, a: Allocator, cache: *std.ArrayList(JsonPathResult)) !void {
    var result = try runQuery(json, qstr, a);
    try cache.append(result);   // cache giữ bản sao con trỏ của result
    defer result.deinit();      // LỖI: giải phóng vùng nhớ mà cache.items đang trỏ tới
    printResults(&result);
}

Trong Rust, hình dạng này không thể biên dịch: cache.push(result) chuyển quyền sở hữu, khiến result không còn tồn tại để gọi drop sau đó.

3. Cấp phát mồ côi khi chuyển vào struct thất bại

pub fn appendBuggy(self: *Iter, v: *Value, path: []const u8) !void {
    const duped = try self.allocator.dupe(u8, path);
    // LỖI: không có errdefer
    try self.cursors.append(self.allocator, .{ .json = v, .path = duped });
}

Nếu append thất bại, chuỗi duped bị mồ côi. Cách sửa: thêm errdefer self.allocator.free(duped); ngay sau khi cấp phát. Trong Rust, Vec::push chuyển giá trị vào và không có API nào trả lại giá trị "đã cấp phát nhưng chưa liên kết" để vô tình làm mất.

Hệ sinh thái còn non trẻ

Tác giả thẳng thắn chỉ ra những hạn chế:

  • Khan hiếm thư viện — ngay cả regex cũng chưa hoàn thiện
  • mvzr — engine regex của Zig — không hỗ trợ \p{...} (Unicode property escapes), gây khó khăn trực tiếp khi triển khai RFC 9535
  • Thư viện chuẩn thay đổi API qua từng phiên bản

Với lập trình viên Việt Nam đang cân nhắc Zig cho dự án mới, đây là những điểm cần lưu ý: nên dùng cho dự án cá nhân hoặc nghiên cứu trước, chưa vội đưa vào sản phẩm thương mại lớn.

Kết luận

Zig để lại ấn tượng tốt: đơn giản, hiện đại và cực nhanh. Tác giả tin ngôn ngữ này có tiềm năng thực sự để trở thành kế thừa xứng đáng của C. Bù lại, Zig còn trẻ — hình hài ngôn ngữ ở nhiều chỗ vẫn chưa hoàn thiện, và sẽ cần thêm tính năng "chất lượng cuộc sống" cùng cú pháp đường (syntax sugar) khi trưởng thành.

Tôi sẽ tiếp tục đóng góp cho hệ sinh thái, bất cứ khi nào gặp dự án đáng để xây dựng.

Đối với cộng đồng lập trình viên hệ thống, câu chuyện này là lời nhắc nhở: Rust và Zig không đối đầu mà bổ sung nhau. Rust ưu tiên an toàn bộ nhớ ngay từ lúc biên dịch; Zig chọn sự đơn giản, minh bạch và tự do cho lập trình viên — đổi lại là trách nhiệm cao hơn với từng byte bộ nhớ.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗