Zig for me is the new C

Zig for me is the new C

An introductory word

On the occasion of the release of version 0.11.0 of the Zig language, I decided to write an article about what attracted me to the language, what I like. The Zig language itself has a number of interesting solutions that set it apart from other “killers” of the C language. Briefly:

  • built-in collection system;

  • direct use of header files written in C;

  • compilation of code written in C by the Zig compiler;

  • implementation comptime code during compilation;

  • memory allocation mechanism with the option of selecting an allocator;

  • and other.

The reason I decided to learn Zig is because I didn’t want to fully learn C. It’s not that I’m not familiar with C. I came across it a long time ago, about twenty years ago, but then I quickly switched to Pascal(Delphi) and then already on C++, because they had more capabilities of the languages ​​themselves compared to C. Now, seeing what C++ is turning into, I decided to take a step back. Pascal and Delphi have gone by the wayside for me. And I decided to return to C, because it is closer to me in terms of syntax, and I myself lean more towards system programming. I was surprised that C has changed little compared to C++. On the one hand, this is good – stability, portability, and so on. But on the other hand, large projects written in C are not only difficult to read, but also difficult to maintain. I somehow decided to work with GTK. And was pleasantly surprised by its internal structure, especially considering Glib with its GObject. In addition to GTK, there were other projects in C, and there, too, it cannot be said that the code is pleasant. Also, with my habit of working in the C++ style, I was annoyed by the duplication of the same words in the function name and in the first parameter of the function that works with the structure data. In general, from the point of view of a C++ programmer, I have many complaints about C, but this is all rhetoric.

WARNING! It should be noted right away that the Zig language is still not ready for full use in commercial code. It is still changing. are marked temporary milestones and versions when certain new features of the language will be added. But it already is projects, which use Zig. And no one forbids using it for their experiments.

WARNING! Documentation for the language is still incomplete. It has almost everything. But something remained fully described. Something has no description at all. If you are interested in any questions, write in the comments, or contact them for any question community from language I also indicated the link at the end of the article.

What attracted me to Zig language so much?

Reading the code

Below is an example of my experiment with raylib. Habr does not know how to embellish Zig code. But even so, the reading is good.

const std = @import("std");
const raylib = @import("raylib");
const ResourceManager = @import("resourcemanager.zig").ResourceManager;
const ScreenManager = @import("screenmanager.zig").ScreenManager;

const allocator = std.heap.c_allocator;

pub fn main() !void {
    const screen_width = 800;
    const screen_height = 450;

    raylib.Window.init(screen_width, screen_height, "Test Window");
    defer raylib.Window.close();

    raylib.AudioDevice.init();
    defer raylib.AudioDevice.close();

    var resouce_manager = try ResourceManager.init(allocator);
    defer resouce_manager.deinit();

    const music = try resouce_manager.loadMusic("resources/ambient.ogg");
    music.setVolume(1.0);
    music.play();

    const sfx_coin = try resouce_manager.loadSound("resources/coin.wav");
    sfx_coin.setVolume(1.0);

    const font = try resouce_manager.loadFont("resources/mecha.png");
    _ = font;

    var screen_manager = try ScreenManager.init(allocator);
    defer screen_manager.deinit();

    const target_fps = 60;
    raylib.setTargetFPS(target_fps);

    while (!raylib.Window.shouldClose()) {
        music.update();

        raylib.beginDrawing();
        defer raylib.endDrawing();

        screen_manager.update();
        screen_manager.draw();
    }
}

There is nothing that raises the question. This is the simplest example. But even if you take something worse, the reading does not fall. This is because “innovations” have not yet been introduced into the language, but the authors indicate:

Favor reading code over writing code.

If interpreted in Russian, it will turn out roughly like this:

The advantage of ease of reading code over ease of writing code.

And so far, the developers are following this approach. There is only one point that stands out for me. This is the construction of the view (taken from the documentation):

var y: i32 = 123;

const x = blk: {
    y += 1;
    break :blk y;
};

Here is the named block blk is an expression that returns a value through break. That is, instead of code y += 1 may be a full-fledged algorithm for calculating something for a variable x. It’s kind of like a lambda, but it’s not a lambda. Construction break :blk y; – this is a block execution stop blk and return value y. Why does it look like this? It’s a mystery to me. I understand how it works, I understand that you can embed other named blocks inside this block, and then return to the beginning of the nested blocks, but the eye always clings to this construction.

Everything is “Structure”

Actually, it’s not quite like that, but to make it easier to understand what attracted me, I’ll label it like that. It’s like in the Lua language, where “Everything is a Table” (table). From the code of my experiment with raylib, it can be seen that by loading the desired module via a builtin function @importthe programmer through an alias, as if addressing the elements of the structure, gets access to all internal fields, structures, unions, enumerations, functions and other elements of this module, which are marked as pubthat is, those that are public. To understand the essence of “pseudonym”, you can draw an analogy with typedef with C or using with C++. In the example above, the function main the same is public. The difference from C is immediately apparent, in Zig code elements are “private” by default, while in C they are “public”. C is the keyword static plays the role of “restrictor”, and it is full of its own features. Access is presented more transparently in Zig. There is only one exception to the general rule in Zig. Fields of structures (struct), unions (union), emunerations (enum) and other similar elements can only be public. That is, the behavior repeats the C language.

Personally, I would like to emphasize that it is very convenient to access the elements of a specific file through a point. I have noted this for myself in other languages, and it is very lacking in C. At the same time, in C++, due to the presence of namespaces, there are no problems. In one of the points below, I will indicate an additional indirect plus of the concept “Everything is a structure”.

Zig has another keyword usingnamespace. If you use it on import, you can load the contents of the file without the alias. Something like that:

usingnamespace @import("file.zig");

And sometimes it is necessary. But I believe that having an alias improves code separation and, accordingly, its readability.

Internal functions in the body of structures, associations, enumerations

In Zig, the body of a structure (struct), union (union) or enumeration (enum) can be written functions. And in fact, the name of the specific element becomes the namespace for the function. Below is one example from my experiment with raylib.

const AudioStream = @import("audiostream.zig").AudioStream;

pub const Sound = extern struct {
    stream: AudioStream,
    frameCount: c_uint,

    pub fn load(filename: [:0]const u8) Sound {
        return LoadSound(@ptrCast(filename));
    }

    pub fn isReady(self: Sound) bool {
        return IsSoundReady(self);
    }

    pub fn update(
        self: Sound,
        data: *const anyopaque,
        sample_count: i32,
    ) void {
        UpdateSound(self, data, @as(c_int, sample_count));
    }

    pub fn unload(self: Sound) void {
        UnloadSound(self);
    }

    pub fn play(self: Sound) void {
        PlaySound(self);
    }

    pub fn stop(self: Sound) void {
        StopSound(self);
    }

    pub fn pause(self: Sound) void {
        PauseSound(self);
    }

    pub fn doResume(self: Sound) void {
        ResumeSound(self);
    }

    pub fn isPlaying(self: Sound) bool {
        return IsSoundPlaying(self);
    }

    pub fn setVolume(self: Sound, volume: f32) void {
        SetSoundVolume(self, volume);
    }

    pub fn setPitch(self: Sound, pitch: f32) void {
        SetSoundPitch(self, pitch);
    }

    pub fn setPan(self: Sound, pan: f32) void {
        SetSoundPan(self, pan);
    }
};

And access to functions described inside a specific element is now possible in two ways (both ways are equivalent):

const Sound = @import("sound.zig").Sound;

const some_sound = Sound.load("somesound.wav");
Sound.play(some_sound); // первый способ
some_sound.play(); // второй способ

The first method works for any functions inside a specific element. The second method works if the variable is an instance of a particular element, and the first parameter of the function is passed an instance of that particular element in which the function resides. For example, in the structure example code Sound the type of the function’s return value load the structure itself Sound. A function play has an input parameter with the type of this structure. That is, in the example code of the function call, the variable some_sound is an instance of a structure Sound. And this means that you can call the corresponding functions of the structure in a simpler way. This is what I miss in C.

Built-in collection system

This is a top. The best idea that came to me. Even Rust (if Cargo is available) and Go are inferior. The bottom line is that the file that controls the assembly of projects for Zig is a file with code in the Zig language, and it itself is compiled by the compiler, after which the project is assembled. At the same time, in the code of the assembly file, you can call third-party programs to perform the necessary operations. There are examples of assembling the utility and its use when compiling the project (the examples from the link are old, the assembly system has changed a little, but you can figure out what it is for).

Below is an example from my same experiment with raylib.

const std = @import("std");
const raylib_zig = @import("raylib-zig/build.zig");

pub fn build(b: *std.Build) void {
    const optimize = b.standardOptimizeOption(.{});
    const target = b.standardTargetOptions(.{});

    const raylib_options = b.addOptions();
    raylib_options.addOption(bool, "platform_drm", false);
    raylib_options.addOption(bool, "raygui", false);

    const exe = b.addExecutable(.{
        .name = "raylib-test",
        .optimize = optimize,
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
    });

    const system_raylib_state = b.option(
        bool,
        "system-raylib",
        "link to preinstalled raylib libraries",
    ) orelse false;

    if (system_raylib_state) {
        exe.linkSystemLibrary("raylib");
    } else {
        exe.linkLibrary(raylib_zig.createCompileStep(b, .{
            .target = target,
            .optimize = optimize,
        }));
    }

    const raylib_lib = b.addModule("raylib", raylib_zig.modules.raylib);

    exe.addModule("raylib", raylib_lib);

    // exe.install();
    const install_exe = b.addInstallArtifact(exe, .{});
    b.getInstallStep().dependOn(&install_exe.step);

    // const run_exe = exe.run();
    const run_exe = std.build.RunStep.create(b, "run raylib-test");
    run_exe.addArtifactArg(exe);

    run_exe.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_exe.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_exe.step);

    // -------------------------------------------------------------- Tests --
    const exe_tests = b.addTest(.{
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&exe_tests.step);
}

I will comment on a couple of places:

// exe.install();
const install_exe = b.addInstallArtifact(exe, .{});
b.getInstallStep().dependOn(&install_exe.step);

The comment states the official simplified version of the two lines below. These lines are essentially needed to set up the build more flexibly. If this is not required, then a line from the comment will be more than enough. I left the complete lines for future experiments.

// const run_exe = exe.run();
const run_exe = std.build.RunStep.create(b, "run raylib-test");
run_exe.addArtifactArg(exe);

An absolutely similar example. The official simplified version is in the comment.

And here I will note another plus, which I indicated earlier. You don’t need to specify everything to compile Zig code .zig files in the assembly file, as in a number of other languages. It is enough to specify only the first (main) file, and the compilation will recursively collect all the required files. That is, if the programmer uses some structure somewhere in the code, but the structure is in another file, then importing a file with this structure, the programmer will not be able to add the code. And this means that he will not be able to lose the same thing on the way. In is convenient.

But it doesn’t work with C code. When compiling, you still need to specify all the ones used .c or .cpp (cc, mm etc.) files. And if this is not done, then the compiler will say that it “does not know such symbols” in the compiled code.

Lack of needed .c (.cpp, cc, mm etc.) halyards is a noticeable problem, especially in large projects. I’ll briefly mention the inverse problem for languages ​​like C and C++, where the code is not needed and the code files are still in the build scripts. The code of these files will not be called, but the files themselves will participate in the compilation. And this means that the compilation time will not decrease and the initial size of the finished file will not decrease.

And why not Rust?

I specifically left the answer to this question at the end. And the answer is banal and simple. Rust is not replaced by C. In general. That doesn’t mean it can’t be used instead of C. No. And even together you can. But not as you can combine with Zig. One of the killer features of Zig is the ability to interact directly with C code. Although Zig is not unique in this. There are a number of other languages ​​(C2, C3, V, Vox, and a few more that I forget the names of) that do pretty much the same thing as Zig. They are trying to replace C as the language of system programming and interprogramming of libraries. In Zig, interaction with C works both ways. That is, you can write a library in Zig, following some rules, which can be used in a project written in C, or in another language where you can upload libraries written in C.

But I liked Zig more. I think he has a future.

Link – link

The main site of the Zig language / He is in Russian
Language documentation version 0.11.0
Standard library documentation
(I recommend reading the code of the library itself, it is very easy to read. Everything is written in the comments of the code as in the web version, since Zig has a built-in generation of documentation from comments. And the code is still easier to navigate)

Language milestones with statuses on Github

The official list of language communities on the github wiki

Telegram chat ziglang_en
Telegram chat ziglang_ru
There is another Russian-language Telegram chat
(They say that the owner of the chat is behaving strangely and that is why this chat was removed from the official list of communities)

Ziggit forum
News feed Zig NEWS

Ziglearn site for learning
Ziglings: Learning through Problem Solving
Zig By Example – Zig code examples
(The examples are simple, and it is recommended to first learn the language itself, because there are no comments to the code in the examples)

There is an r/zig subreddit, but it is now read-only after the well-known events of the shutdown of the free redda API.

Related posts