Zig для меня — это новый C

в 13:47, , рубрики: C, raylib, zig, Программирование, системное программирование
Zig для меня — это новый C - 1

Вводное слово

По случаю выхода версии 0.11.0 языка Zig я решил написать статью о том, что привлекло меня в языке, что мне в нём нравится. Сам язык Zig имеет ряд интересных решений, которые выделяют его на фоне других «убийц» языка C. Коротко:

  • встроенная система сборки;

  • прямое использование заголовочных файлов написанных на C;

  • компиляция кода написанного на C компилятором Zig;

  • выполнение comptime кода во время компиляции;

  • механизм аллокации памяти с возможностью выбора аллокатора;

  • и другие.

Причина, по которой я решил изучать Zig - я не захотел полноценно учить C. Не то, чтобы я не знаком с C. Я с ним столкнулся очень давно, лет двадцать назад, но тогда я быстро переключился на Pascal(Delphi), а потом уже на C++, так как у них было больше возможностей самих языков в сравнении с C. Сейчас же, видя во что превращается C++, я решил сделать шаг назад. Pascal и Delphi ушли для меня в сторону. И я решил вернуться к C, так как по синтаксису он мне ближе, и я сам больше в сторону системного программирования склоняюсь. Я был удивлён тому, что язык C в сравнении с C++ изменился мало. Это с одной стороны хорошо - стабильность, портируемость и всё такое. Но с другой стороны - большие проекты написанные на C не только сложно читать, но и сложно поддерживать. Я как-то с GTK решил поработать. И был неприятно удивлён его внутренней структурой, особенно с учётом Glib с его GObject. Помимо GTK были и другие проекты на C, и там тоже не сказать, что приятный код. А ещё с моей привычкой работать в C++ стиле, меня бесило дублирование одних и тех же слов в названии функций и в первом параметре функции, которая работает с данными структуры. В общем, с точки зрения C++ программиста у меня много претензий к C, но это всё лирика.

Внимание! Стоит сразу обозначить, что язык Zig всё ещё не готов для полноценного использования в коммерческом коде. Он до сих пор меняется. Есть обозначенные временные вехи и версии, когда будут добавлены те или иные новые возможности языка. Но уже есть проекты, которые используют Zig. И никто не запрещает использовать его для своих экспериментов.

Внимание 2! Документация для языка всё еще не полная. В ней есть практически всё. Но что-то осталось не полностью описано. Что-то вовсе не имеет описания. Если вас интересует какие-то вопросы, пишите в комментариях, или обращайтесь с ними в любое сообщество по языку. Ссылки я также указал в конце статьи.

Чем же меня так привлёк язык Zig?

Читаемость кода

Ниже пример из моего эксперимента с raylib. Habr не умеет разукрашивать код Zig. Но даже так читаемость хорошая.

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();
    }
}

Нет ничего, что вызывало бы вопросы. Это простой пример. Но даже если взять что-то похлеще, то читаемость не падает. Это потому, что пока ещё в язык не завезли «новшества», но авторы указывают:

Favor reading code over writing code.

Если интерпретировать на русский, то выйдет примерно так:

Предпочтение удобства чтения кода перед удобством написания кода.

И пока что разработчики соблюдают этот подход. Есть только один момент, который для меня совсем выбивается из общего вида. Это конструкция вида (взято из документации):

var y: i32 = 123;

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

Здесь именованный блок blk представляет собой выражение, которое возвращает значение через break. То есть вместо кода y += 1 может быть полноценный алгоритм вычисления чего-то для переменной x. Это что-то типа лямбды, но это не лямбда. Конструкция break :blk y; - это остановка выполнения блока blk и возврат значения y. Почему имеет такой вид? Для меня загадка. Я понимаю как это работает, понимаю, что можно внутри этого блока встроить ещё именованные блоки, и потом возвращаться к началу из вложенных блоков, но глаз каждый раз цепляется за эту конструкцию.

Всё есть «Структура»

На самом деле это не совсем так, но для упрощения понимания то, что меня привлекло я обозначу это именно так. Это примерно, как в языке Lua, где «Всё есть «Таблица» (table). Из кода моего эксперимента с raylib видно, что загрузив нужный модуль через встроенную функцию @import, программист через псевдоним, как бы обращаясь к элементам структуры, получает доступ ко всем внутренним полям, структурам, объединениям, энумерациям, функциям и другим элементам этого модуля, которые обозначены как pub, то есть те, что являются публичными. Для понимания сути «псевдонима» можно провести аналогию с typedef из C или using из C++. В примере выше функция main то же публичная. Сразу видно отличие от языка C, в Zig элементы кода по умолчанию «приватные», в то время как в C они «публичные». В C ключевое слово static играет роль «ограничителя», и оно полно своих особенностей. В Zig же более прозрачно представлен доступ. Есть только одно отличие от общего правила в Zig. Поля структур (struct), объединений (union), эмунераций (enum) и других похожих элементов могут быть только публичными. То есть поведение повторяет язык C.

Лично от себя отмечу, что получить доступ через точку к элементам конкретного файла это очень удобно. Я отметил это для себя и в других языках, и этого очень не хватает в C. При этом в C++ из-за наличия пространств имён не ощущается проблем. В одном из пунктов ниже я обозначу дополнительный косвенный плюс концепции «Всё есть «Структура».

Есть в Zig ещё ключевое слово usingnamespace. Если его использовать при импорте, то можно загрузить содержимое файла без псевдонима. Примерно так:

usingnamespace @import("file.zig");

И иногда это необходимо. Но я считаю, что наличие псевдонима улучшает разграничение кода и соответственно его читаемость.

Внутренние функции в теле структур, объединений, энумераций

В Zig в тело структуры (struct), объединения (union) или в энумерации (enum) можно вписывать функции. И по сути имя конкретного элемента становится пространством имён для функции. Ниже ещё один пример из моего эксперимента с 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);
    }
};

И доступ к функциям описанных внутри конкретного элемента теперь возможен двумя способами (оба способа равнозначны):

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

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

Первый способ работает для любых функций внутри конкретного элемента. Второй способ работает, если переменная является экземпляром конкретного элемента, и первым параметром функции передаётся экземпляр этого конкретного элемента, внутри которого находится функция. Например, в коде примера структуры Sound тип возвращаемого значения функции load сама структура Sound. А функция play имеет входной параметр с типом этой структуры. То есть в коде примера вызова функций переменная some_sound - это экземпляр структуры Sound. А это значит можно вызывать подходящие функции структуры способом попроще. Это то, что мне очень не хватает в C.

Встроенная система сборки

Это топчик. Лучшая идея, что встречалась мне. Даже Rust (при наличии Cargo) и Go уступают. Суть в том, что файл, который управляет сборкой проектов для Zig - это файл с кодом на языке Zig, и он сам компилируется компилятором, после чего собирается проект. При этом в коде файла сборки можно вызывать сторонние приложения для выполнения необходимых операций. Есть примеры сборки утилиты, и её использовании при компиляции проекта (примеры из ссылки старенькие, система сборки немного изменилась, но разобраться что к чему можно).

Ниже пример из всё того же моего эксперимента с 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);
}

Прокоментирую пару мест:

// 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);

Абсолютно аналогичный пример. В комментарии официальный упрощённый вариант.

И здесь я отмечу еще один плюс, о котором я указывал ранее. Для компиляции кода на языке Zig не нужно указывать все .zig файлы в файле сборки, как в ряде других языков. Достаточно указать только первый (основной) файл, а компиляция рекурсивно соберёт все файлы, которые нужны. То есть, если программист использует некую структуру где-то в коде, но структура находится в другом файле, то импорт файла с этой структурой программист не сможет не добавить в код. А это значит и потерять по пути то же не сможет. У - удобно.

Но это не работает с кодом на языке C. При сборке всё равно необходимо указывать все используемые .c или .cpp (cc, mm и др.) файлы. И если этого не сделать, то компилятор скажет, что «не знает таких символов» в компилируемом коде.

Нехватка нужных .c (.cpp, cc, mm и др.) фалов заметная проблема, особенно в больших проектах. Мельком упомяну обратную проблему для языков типа C и C++, когда код не нужен, а файлы с кодом всё еще находятся в скриптах сборки. Код из этих файлов вызываться конечно не будет, но сами файлы в компиляции учавствовать будут. А это значит, что время компиляции не сократиться, и выходной размер готового файла не уменьшится.

А почему не Rust?

На этот вопрос я специально оставил ответ на конец. И ответ банален и прост. Rust не замена C. Вообще. Это не значит, что его нельзя использовать вместо C. Отнюдь. И даже совместно можно. Но не так, как можно С совмещать с Zig. Одна из киллер фитч языка Zig, возможность напрямую взаимодействовать с кодом языка C. Хотя Zig в этом не уникален. Есть ряд других языков (C2, C3, V, Vox, и ещё несколько языков, названия которых я забыл), которые делаю практически то же самое, что и Zig. Пытаются заменить C, как язык системного программирования и межпрограммного использования библиотек. В языке Zig взаимодействие с языком C работает в обе стороны. То есть можно написать библиотеку на Zig, соблюдая некоторые правила, которую можно будет использовать в проекте написаном на C, или на другом языке, где можно подгружать библиотеки написанные на C.

И Zig мне понравился больше. Мне кажется, что у него есть будущее.

Ссылки - ссылочки

Основной сайт языка Zig / Он же на русском
Документация языка версии 0.11.0
Документация стандартной билиотеки
(Рекомендую читать код самой библиотеки, она читается очень просто. В комментариях кода написано всё тоже самое, что и в веб версии, так как Zig имеет встроеную генерацию документацию из комментариев. И по коду всё же проще ориентироваться)

Важные вехи языка со статусами на Github

Официальный список сообществ по языку в wiki на github

Телеграм чат ziglang_en
Телеграм чат ziglang_ru
Есть еще один русскоязычный Телеграм чат
(Говорят там владелец чата странно себя ведёт и поэтому этот чат удалили из официального списка сообществ)

Форум Ziggit
Новостная лента Zig NEWS

Сайт ziglearn для обучения
Ziglings: обучение через решение проблем
Zig By Example - примеры кода на Zig
(Примеры простенькие, и рекомендуется для начала поизучать сам язык, так как комментариев к коду в примерах нет)

Есть сабреддит r/zig, но он теперь только для чтения после известных событий с закрытием бесплатного API реддита.

Автор:
AnimeSlave

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js