Sử dụng Zig toolchain cho C/C++

Trước giờ mình cũng muốn viết 1 về Zig lâu rồi. Nhân dịp BunJs nổi lên thì mình cũng muốn chia sẻ đến các bạn ngôn ngữ lập trình ZigZig toolchain (zig cc).

Theo như tiêu đề thì mình sẽ không đề cập đến phần ngôn ngữ lập trình của Zig mà chỉ đề cập đến Toolchain thôi.

Giới thiệu sơ lược về C/C++ compiler

Như các bạn đã biết, để compile C/C++ code thì ta cần C/C++ compiler như:

đa phần hiện giờ là đang sử dụng 3 loại compiler chính là gcc (Linux), clang (Linux, Macos, BSD) và msvc (Windows).

Giới thiệu về Zig

Zig là ngôn ngữ lập trình đa mục đích, nhanh, tối ưu và cũng là toolchain được viết bởi Andrew Kelley.

Lưu ý: Zig vẫn đang trong quá trình phát triển và chưa đến version 1.0.0 (hiện giờ là version 0.10.0-dev) nên sẽ có những bugs và breaking changes (cho những bạn lập trình ngôn ngữ Zig).

Với những bạn như mình, thích xài những thứ tối tân nhất, version mới nhất và không cần cài đặt nhiều thì Zig cũng sẽ phù hợp với bạn.

Đồng thời việc chỉ dạy sẽ dễ dàng hơn khi chỉ cần download và extract là đã có được C/C++ compiler trong máy. Cho những bạn thích theo hướng tối giản, chỉ cần code editor (Vim, NeoVim, VsCode, Kakoune, Helix, etc.) và compiler.

Download

Các bạn có thể download tại đây hoặc sử dụng zigup (tương tự như rustup).

Thêm Zig vào $PATH (hoặc %PATH%) và chạy thử

$ zig --help
Usage: zig [command] [options]

Commands:

  build            Build project from build.zig
  init-exe         Initialize a `zig build` application in the cwd
  init-lib         Initialize a `zig build` library in the cwd

  ast-check        Look for simple compile errors in any set of files
  build-exe        Create executable from source or object files
  build-lib        Create library from source or object files
  build-obj        Create object from source or object files
  fmt              Reformat Zig source into canonical form
  run              Create executable and run immediately
  test             Create and run a test build
  translate-c      Convert C code to Zig code

  ar               Use Zig as a drop-in archiver
  cc               Use Zig as a drop-in C compiler
  c++              Use Zig as a drop-in C++ compiler
  dlltool          Use Zig as a drop-in dlltool.exe
  lib              Use Zig as a drop-in lib.exe
  ranlib           Use Zig as a drop-in ranlib

  env              Print lib path, std path, cache directory, and version
  help             Print this help and exit
  libc             Display native libc paths file or validate one
  targets          List available compilation targets
  version          Print version number and exit
  zen              Print Zen of Zig and exit

General Options:

  -h, --help       Print command-specific usage

như các bạn có thể thấy thì có những sub-commands như cc, c++, ar, lib, ranlib, dlltool (và clang nhưng không được hiển thị trong zig --help).

Zig sử dụng clang để compile cũng như translate C/C++ code và được ship với nhiều header files (Glibc, Musl, Mingw, etc.) để tiện hơn cho việc cross-compile.

Các bạn cũng có thể làm cross-compile với clang, nhưng setup sysroots thôi cũng sẽ khiến các bạn đau cả đầu rồi. :)

Sử dụng

C

#include <stdio.h>

int main(int argc, char** argv) {
    printf("Hello %s\n", "world");
}
$ zig cc main.c -o hello-world
$ ./hello-world
Hello world

C++

#include <iostream>

int main(int argc, char** argv) {
    std::cout << "Hello world\n";
}
$ zig c++ main.cpp -o hello-world
$ ./hello-world
Hello world

Ban đầu chạy các bạn sẽ thấy lâu 1 chút vì Zig compile Libc from source, nhưng sau khi xong lần đầu thì mọi thứ sẽ được cached lại nên những lần sau sẽ nhanh hơn.

Lưu ý

Zig bật UBSAN by default, nên nếu có lỗi khi sử dụng zig cc mà không bị trên những compiler khác, hãy thử pass flag -fno-sanitize=undefined.

Why do i get illegal instruction when using zig cc to build C code

Đồng thời, vì Zig vẫn đang tròn quá trình phát triển nên sẽ có những clang flags không được hỗ trợ và những vấn đề khác về -mcpu, -march, -mtune.

Cross-compile

Cross-compile khi bạn muốn compile chương trình của mình lại cho 1 hệ điều hành khác hoặc máy khác như từ Linux -> Windows hoặc từ Linux -> Macos hoặc từ Windows -> Macos hoặc từ Glibc Linux -> Musl Linux.

Hoặc chỉ đơn giản bạn muốn chương trình statically compiled thay vì linking với Libc trên Linux (việc statically compiled trên Glibc gần như là không thể, nhưng Musl thì được).

Bạn chỉ cần pass flag với triple như -target arch-os-abi như

  • -target x86_64-windows-gnu (sử dụng Mingw)
  • -target x86_64-windows-msvc (cần MSVC cài đặt trên máy)
  • -target x86_64-linux-gnu
  • -target x86_64-linux-gnu.2.19 (Glibc v2.19)
  • -target x86_64-linux-gnu.2.28 (Glibc v2.28)
  • -target x86_64-linux-musl
  • -target x86_64-macos
  • -target aarch64-linux-musl
  • -target native-native-gnu
  • etc.

để xem thêm, các bạn hãy sử dụng zig targets.

Những projects dùng Zig (zig cc)

Đặc biệt phải nhắc đến là Uber cũng sử dụng zig cc và trong video cũng có nhắc đến Cloudflare cũng có sử dụng qua bazel-zig-cc.

6 Likes

lần đầu viết bài
đọc lại thấy không đầu không đít :3

Người ta dùng Zig giải quyết bài toán/vấn đề gì hả bạn? Bạn có thể nêu một ví dụ để khuyến khích mọi người học và sử dụng Zig thay vì native C/C++ không?

3 Likes

nếu bạn có thời gian thì có thể xem qua 2 clip của Andrew giới thiệu về Zig


Ngoài thì cũng có 1 bài Why Zig When There is Already C++, D, and Rust?

Theo trên Hackernews thì người ta hay ví

Zig is to C and Rust is to C++
Muốn better-C -> Zig
Muốn better-C++ -> Rust

Thì Zig như C, ngôn ngữ đơn giản, nếu bạn đã quen C rồi thì chuyển sang Zig cũng khá nhanh chóng. Chỉ là thay đổi về syntax với học thêm 1 chút concept mới như slice thôi.

No hidden control flow

Zig không có macro như C (#define, #ifdef, etc.), đối với những function mà theo kiểu Os-dependent thì có thể giải quyết qua lazy evaluation và compile-time known.

Lazy evaluation

là những hàm nào được gọi mới sẽ compile, không thì thôi.

  • Pros: Compile nhanh, không bị include những function mà mình không dùng đến
  • Cons: Do function ko được compile nên đến khi bạn dùng đến thì mới bắt đầu báo lỗi tè le :)

Điều này giúp Zig build static binary nhưng chiếm ít size (đa phần là bé hơn cả C).

Compile-time known (comptime)

là gần như giải quyết đa số các vấn đề cần đến macro hay generic. Khi bạn build app cho Windows thì tại thời điểm biên dịch, bạn đã biết Os bạn cần build rồi, với lazy evaluation thì compiler sẽ chỉ compile các funtions cho Windows và không đụng chạm gì đến các functions cho Linux hay Macos.

Còn về generic thì đây là 1 ví dụ:

pub fn Node(comptime T: type) type {
    return struct {
        const Self = @This();

        data: T,
        next: ?*T = null,

        pub fn init(data: T) Self {
            return Self{ .data = data };
        }
    };
}

const NodeU64 = Node(u64);
// same as
// const NodeU64 = struct {
//     data: u64,
//     next: ?*u64,
// };

var node = NodeU64.init(32); // Node(u64).init() cũng vẫn được

thì bạn đã làm ra 1 generic struct (như <template T> trong C++)

đây cũng là cách khá thông dụng cho C developer sử dụng generic trong C qua macro (real-world project)
họ cũng tạo ra macro rồi pass vào itemType hay T như ví dụ vừa rồi.

Và bạn cũng có thể gọi đa số các hàm với prefix comptime như

var fib_25 = comptime fibonacci(25);

để gọi hàm tại compile-time thay vì viết 1 script khác để tính rồi copy & paste kết quả vào.

Nhưng comptime không can thiệp AST (như Rust hay Nim) nên để giải quyết vấn đề này thì bạn nên có build script.
Và allocating, syscall, etc. sẽ không thể sử dụng comptime được nha!

No hidden memory allocations

Zig là ngôn ngữ memory manually management như C, nên bạn sẽ phải tự mình allocatefree memory.

Zig không có global allocator như malloc, calloc hay free trong C. Thay vào đó, để allocating memory thì bạn cần pass 1 argument là Allocator

// Cấp phát vùng nhớ heap (100 items) và fill toàn bộ bằng số 69
pub fn fill(allocator: Allocator) !void {
    var slice = try allocator.alloc(u64, 100);
    defer allocator.free(slice);

    for (slice) |*item| {
        item.* = 69;
    }
}

việc passing allocator sẽ giúp bạn đa dạng hoá hơn trong việc chọn allocator phù hợp và dễ dàng thay đổi.

Ví dụ như trong C, bạn viết ArrayList. Bạn sài mallocfree rất bình thường. Cho đến khi bạn xài đoạn code đó trên WASM thì sẽ bị lỗi, do WASM không hề có mallocfree.

Bằng việc passing allocator như vậy, bạn có thể dễ dàng viết 1 custom allocator phù hợp cho mục đích sử dụng.

Trong stdlib (std.heap) cũng có nhiều loại allocator như

  • page_allocator để alloc 1 page (thường là khá lớn)
  • c_allocatorraw_c_allocator để làm việc với C libraries và thường có hiệu năng cao.
  • ArenaAllocator để có thể free các bộ nhớ cùng 1 lúc
  • failing_allocator sẽ tự động lỗi khi cấp phát bộ nhớ, đee test xem bạn có handle việc cấp phát bộ nhớ lỗi đúng chưa.
  • GeneralPurposeAllocator để debug code, kiểm tra memory leaks.
  • std.testing.allocator là 1 instance của GeneralPurposeAllocator để khỏi setup phức tạp, có sẵn đó để pass luôn.

Thì cùng 1 function, 1 đoạn code, bạn có thể pass nhiều allocator cho nhiều mục đích khác nhau.

C interop

Zig có subcommand là translate-c để dịch từ 1 đoạn C code -> Zig code (đa phần là header file).
Miễn trong .h file không chứ những đoạn macro phức tạp mà chỉ đơn giản như là #define WIDTH 100 thì Zig có thể translate được.

translate-c hoạt động dựa trên libclang. Nên ổng mới nghĩ là đã sử dụng libclang là gần như đã sử dụng nguyên Clang compiler rồi, nên ổng mới expose ra subcommand zig cczig c++ để compile C code.

Trong đó có built-in function là @cImport()@cDefine() (sử dụng translate-c) để sử dụng C library mà không cần viết bindings hay wrapper vì translate-c sẽ lo hết mọi chuyện.

Đây là ví dụ sử dụng Raylib với Zig

// https://ziglang.org/learn/samples

// build with `zig build-exe cimport.zig -lc -lraylib`
const ray = @cImport({
    @cInclude("raylib.h");
});

pub fn main() void {
    const screenWidth = 800;
    const screenHeight = 450;

    ray.InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window");
    defer ray.CloseWindow();

    ray.SetTargetFPS(60);

    while (!ray.WindowShouldClose()) {
        ray.BeginDrawing();
        defer ray.EndDrawing();

        ray.ClearBackground(ray.RAYWHITE);
        ray.DrawText("Hello, World!", 190, 200, 20, ray.LIGHTGRAY);
    }
}

Có thể thấy là việc sử dụng khá dễ, tương thích với C library.

Build script

Thì trong C/C++, thì bạn có thể đau đầu trong việc chọn build script như Makefile, autogen, Cmake, Gmake, Bmake, Ninja, Samurai, gn, meson, etc.

Thì Zig có build script là build.zig (sử dụng zig init-exe hay zig init-lib để xem mẫu).
build.zig cũng sử dụng ngôn ngữ Zig nên bạn không cần học 1 loại ngôn ngữ khác để viết build script.

Zig cũng đang có dự định về package manager, nhưng vẫn chưa được implemented

Kết

Thì Zig vẫn còn đang trong quá trình phát triển nên sẽ có bug, syntax changes, stdlib changes, language features nên nếu bạn nào muốn học thì cũng cần lưu ý.

Sau khi Zig release phiên bản ổn định (v1.0.0) thì sẽ không thêm, bớt hay thay đổi gì nữa, chủ yếu chỉ fix bug và keep it minimal just like C (đã ổn định được gần 20 năm nay, ngoại trừ C23 ra :))

2 Likes

ngoài ra thì cũng còn khá nhiều như

Error union

Như Result<T, E> trong Rust, tiện trong việc handle error. Thay vì phải return enum như trong C, rồi giá trị cần trả về thì lại phải pass pointer.

defererrdefer

Gần giống defer trong Go, nhưng sẽ chạy vào cuối block/scope thay vì cuối function như Go.

Dễ hơn trong việc initialize và deinitialize (như alloc/free, create/destroy, init/deinit, etc.)

Với pattern

var window = raylib.InitWindow();
defer raylib.CloseWindow(window);

thay vì phải chạy xuống cuối function để ghi CloseWindow() rồi kéo lên lại, khiến cho dễ quên hơn trong việc deinitialize dữ liệu và không cần nhiều đoạn if dài dòng để handle case, vd như trong Spine.

spBinary binary = spBinary_create(atlas)
if (!binary) {
    printf("Error");
    spBinary_dispose(binary); // 1 cái
    return;
}

spSkeletonData skeleton_data = spBinary_createSkeletonData(binary, "file.skel");

spBinary_dispose(binary); // 2 cái
var binary = c.spBinary_create(atlas);
defer c.spBinary_dispose(binary);

if (binary == null) {
    return error.FailedToInitBinary;
}
var skeleton_data = c.spBinary_createSkeletonData(binary, "file.skel");

errdefer cũng tương tự như defer, nhưng chỉ khởi chạy khi chương trình return error.

Wrapping operator

Có thể bạn chưa biết thì trong C, khi 1 biến dữ liệu kiểu số nguyên như int khi đạt tới giá trị tối đa (như 2^32) thì khi bạn tiếp tục + rồi gán lại

  • signed int sẽ bị segfault
  • unsigned int sẽ bị wrap lại về 0 rồi cộng tiếp :)

Tưởng tượng ngân hàng mà nhiều tiền quá xong nạp thêm vào mà biến thành 0đ xem :)
Thà lỗi hệ thống còn hơn mất tiền mà không ai biết.

Thì Zig sẽ ra lỗi và stacktrace cho debug build và segfault cho release build đối với cả signed int unsigned int (hay i32u32 cho đúng kiểu dữ liệu bên Zig)

Nếu bạn muốn wrap lại về 0, thay vì dùng +, hãy dùng +% (tương tự cho -/-%, */*%)

Ngoài wrapping operator ra thì Zig cũng có saturating operator +|, -|, *|. Tức là khi đạt giá trị tối đa, thay vì wrap về lại 0 thì nó sẽ giữ cho biến ở giá trị tối đa (cũng như tối thiểu nếu xài -|).

1 Like

Trong khi noiryuh đang say sưa về những đặc sắc ngôn ngữ của Zig trên đây, Jarred Sumner đang phải vật lộn lèo lái con thuyền Bun JS ra khỏi phao số 0.

Bun’s priorities · Issue #798 · oven-sh/bun · GitHub

Bun’s Roadmap · Issue #159 · oven-sh/bun · GitHub

Bun don’t have full support for Windows yet · Discussion #310 · oven-sh/bun · GitHub

A language is something (or nothing). An ecosystem is everything!

Với Zig nhìn chung mà nói thì những ngôn ngữ thuộc lứa sinh sau đẻ muộn và cũng muốn nhảy vào giải quyết những vấn đề của C/C++, nó cũng đã có đủ và làm tốt. Giờ package manager hầu như là chuẩn, không support C interop trực tiếp cũng có FFI, standard library thì cũng trendy mấy thứ như slice, zip, function-first, Python-like, SIMD các kiểu. Macros thì đám Lisp-like và Lisp-inspired cũng đã giải quyết đủ và tốt, hoặc như Nim, macros của nó dựa hoàn toàn vào sửa AST, khá mạnh. GC là design philosophy, nó không phải là điểm mạnh yếu. Tùy use case. Go, Nim vẫn viết kernel được và vẫn có GC. ref count cũng tính là một loại GC đấy, nói chung thì cũng phải có mechanism để quản lý lifetime. Build system, result type, defer, coroutines, type safety… thì không nói, hack together được hết.

Cũng đang follow Andrew, 1 - 2 tuần trước thấy ổng còn khoe tiến độ làm self-bootstrap compiler :smiley: . Nói chung cũng khá thú vị, nào có thời gian cũng ngâm thử xem. Ngặt nỗi rừng ngôn ngữ C wannabes thì nhiều vô kể, thời gian thì có hạn.

1 Like

thì đang nói về điểm nào hơn C/C++ thì nói tốt thôi. Nếu nói về cons thì nói thật, ngôn ngữ quá verbose, stdlib thì thiếu tùm lum (như http request (có hzzp, mà mấy ông python cũng toàn xài requests thôi chứ thấy ít ai xài urlib hay gì đó), không có json cho Reader, std.io.Reader/Writer nhưng lại phải pass anytype, ReaderreadUntilDelimiterArrayList nhưng lại không có BoundedArray version, json không stringify được StringHashMap, Reader phải đọc bằng Int rồi @bitCast sang Float, etc.)

Vẫn còn nhiều điều cá nhân t cũng không thích lắm ở Zig.
Nhưng do ngôn ngữ vẫn chưa được v1.0.0 nên t cũng ko có ý kiến lắm :)
Với vẫn còn nhiều breaking changes như pinned struct, const = fn (), có khi sẽ có fixed-point number (#1974), khá thú vị :)

Nhưng nếu thấy Zig quá verbose thì có thể thử Odin. Hỗ trợ specific endian int, matrix, complex number, quaternion (gần như đủ case để khỏi cần operator overloading)
Với ngược lại với Zig theo hướng minimalistic thì Odin theo kiểu support mọi thứ kiểu battery-included (vendor opengl, vulkan, directx glfw, botan, raylib, stb, etc. wrapper sẵn trong official repo luôn)

đúng thật là ecosystem của Zig khá nhỏ, nên mới có cái translate-c@cImport để dựa vào kho thư viện đồ sộ của C (và C++).

1 Like

rất thích tôn chỉ tối giản của ngôn ngữ này. Mình nghĩ ai thích C (not C++) sẽ thích Zig

1 Like

Bài viết đầu như vậy là quá ổn.

P/s: Thấy trong Discord đề xuất combo go + rust + zig khá ok, go cho high level, rust khi cần safety và zig khi cần perf

1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?