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

По случаю выхода версии 0.11.0 языка Zig я решил написать ещё одну статью о языке Zig. В этот раз речь пойдет о системе сборки языка. А точнее, как пользоваться кодом написанным на языке C в проекте на языке Zig, с небольшими ответвлениями в стороны для описания некоторых возможностей системы сборки. Тем более, что она претерпела несколько изменений, о чём я так же коротко упомяну. Эксперименты проводились мной на Windows 11. Стоит сразу упомянуть, что указанные в статье команды будут работать и на других операционных системах. Это одна из особенностей языка Zig. Но пример кода линковки системных библиотек для сборки библиотеки raylib будет платформозависимый, так как для разных платформ набор библиотек отличается.

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

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

Глава 1. Система сборки Zig

У языка Zig всего один инструмент для сборки - это компилятор. Он же выступает и генератором проекта, и конвертером кода написанного на C в Zig, и менеджером пакетов, и «линкером». Такой «швейцарский нож». Плохо ли это или хорошо зависит от личных предпочтений программиста. Моё мнение, что это удобно. Стал бы я писать об этом статью. Хотя с другой стороны нарушается принцип KISS. Но я давно уже не видел адекватное ПО, созданного с учетом KISS принципов.

В этой главе я коротко опишу возможности «компилятора» Zig. Если запустить в терминале (консоли) программу zig, то она отобразит список доступных команд:

info: 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
  objcopy          Use Zig as a drop-in objcopy

  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

error: expected command argument

И в конце руганётся, что программа ожидала команды. И всё верно, я ей ничего не предоставил. Если ты, дорогой читатель, не хочешь видеть такую ошибку в конце, то тогда тебе стоит добавить опцию --help. Отобразиться всё то же самое, но без ошибки. При этом, если добавить эту опцию к команде, то компилятор напишет специфичные опции для этой конкретной команды.

И так...

Генератор проекта

Чтобы упростить себе написание примеров дальше я начну с создания нового проекта, а для этого продемонстрирую простой функционал «компилятора» Zig - генерацию проектов. По сути там всего две команды: init-exe и init-lib.

Первая команда генерирует шаблон для создания исполняемого приложения, вторая - шаблон статической библиотеки. Сгенерировать шаблон проекта динамической библиотеки нельзя, такой команды нет. Но можно сгенерировать проект статической библиотеки, и руками поправить код в исходниках для работы с динамической библиотекой. Чувствуется эдакая недоделанность. Но чё нам, зигуанам. Это может быть будет поправлено в будущем. Или нет. Who knows, как говорится.

В Rust и ряде других языков, в которых есть похожий функционал во внешних приложениях, команда генерации проекта может принимать аргумент в виде названия папки (или даже пути), и такая папка создастся автоматически. В Zig папку для проекта нужно создавать самостоятельно заранее. Это как с git. Где git init вызвали, там он репозиторий и создал. Вопрос удобства имеет неоднозначный ответ. Мне лично без разницы, но вроде как в одном из будущих релизов компилятора Zig добавят возможность делать как в Rust, указывать название папки, в которой будет создан проект. Но это мы отвлеклись.

Я создал папку и выполнил zig init-exe внутри неё. Созданный проект имеет бесхитростную структуру:

src\main.zig
build.zig

Тут же можно дернуть упомянутый ранее git init. Но я не буду, так как это просто статья с примерами и контроль версий мне в ней не нужен. Да и я сам чаще стал пользоваться fossil.

Если выполнить zig init-lib будет всё то же самое:

src\main.zig
build.zig

Отличаться в этом случае будет только наполнение файлов.

Ничего более детального здесь писать нет смысла, так как это всё, что пока есть в функционале генератора проектов. Создав шаблон, время перейти к...

Компилятор

Стоит сразу упомянуть, что по умолчанию компилятор имеет настройки для дебага. Чтобы это изменить нужно указать опцию -O <режим> (о большая), где <режим> - один из четырёх доступных на данный момент:

  • Debug (режим по умолчанию) включит все улучшения безопасности, не будет делать никаких (кроме некоторых стандартных) оптимизаций, и добавит дебажные символы.

  • ReleaseFast отключит все улучшения безопасности, и применит все оптимизации для ускорения выполнения кода.

  • ReleaseSafe включит все улучшения безопасности, и применит большую часть оптимизации для ускорения кода.

  • ReleaseSmall отключит все улучшения безопасности, и применит оптимизации для уменьшения конечного размера готового файла.

Тут так же стоит упомянуть, что не стоит ожидать, что результат при использовании опции ReleaseFast будет всегда быстрым, ReleaseSafe будет всегда безопасным, а ReleaseSmall будет всегда маленьким. Эти опции лишь указание компилятору какие настройки применить при компиляции кода. Конечный результат зависит от самого кода.

В общем, компилятор. У программиста есть два пути (я конечно же хотел пошутить про стулья, но мы здесь взрослые люди) компиляции.

Путь №1. Самый простой

zig build

И всё! Если есть ошибки, то в терминале (консоли) компилятор насыпет их, и они будут вполне понятными и читаемыми (одно из преимуществ языка Zig перед языком C). Если ошибок нет, то и писать нечего. А так как я использую готовый шаблон, то ошибок в нём нет, а значит в терминале компилятор ничего не отобразит, когда выполнит сборку.

При сборке простым путём используется файл build.zig как «скрипт» системы сборки. В нём прописываются правила сборки, всё как в других системах сборки. Без этого файла команда zig build работать не будет.

При стандартных настройках во время компиляции в папке проекта создаются две папки: zig-cache и zig-out. Учитывай это, дорогой читатель, когда будешь продумывать структуру папок проекта. Исходя из названий папок можно понять, что zig-cache - это папка с кэшем сборки, а zig-out - папка с исполняемым файлом. Точнее с папкой bin, внутри которой будет находится готовый исполняемый файл.

Кончено же команда zig build не конец. Внутри файла build.zig указаны шаги сборки и запуска run и test, которые соберут и выполнят ту, часть кода, которая нужна. Но приятно, когда можно собрать, что нужно простым способом.

О файле build.zig я напишу ещё раз далее. А пока...

Путь №2. Как в старые добрые...

zig build-exe src/main.zig

В терминале (консоли) всё будет точно так же - без ошибок. Но компиляция пройдёт несколько иначе. Во первых, создастся только одна папка zig-cache. Во вторых, готовый исполняемый файл (или библиотека) будет размещён в той же папке, из которой мы вызвали команду на компиляцию, и название этого файла будет таким же как название файла с кодом, который мы указали при компиляции (чтобы поменять название достаточно добавить опцию --name <имя>, где вместо <имя> указать требуемое название).

И снова упомяну мной любимую особенность. В отличие от языка C в языке Zig не нужно указывать другие файлы с кодом языка Zig, которые используются в проекте, так как они будут автоматически собраны компилятором, так как они используются в коде. А вот с файлами языка C указывать придётся. Но команда при этом не поменяется:

zig build-exe -lc src/main.с

Добавиться только опция -lc, она же --library c. Указывает линкеру, что нужно добавить стандартную C библиотеку. При этом опция -I тоже никуда не делась, если нужны заголовочные файлы. То есть функционал аналогичен популярным компиляторам языка C.

Заметьте, такой способ вполне подходит для использования для сторонних систем сборки, если такое требуется. Г - Гибкость.

Конвертер проекта на языке C в язык Zig

Такой функционал у компилятора тоже есть, но для него нужно наверное отдельную статью писать, так как конвертер полон своих нюансов. Хотя может быть будет достаточно указать простой пример, чтобы ты, дорогой читатель, понял механизм работы и некоторые проблемы конвертера. Например, простой код на языке C взят мной из wikipedia:

#include <stdio.h>

int main(void)
{
    printf("Hello, world!\n");
    return 0;
}

Для конвертации я сохранил код примера в файле с названием main.c и в терминале (консоли) выполнил команду:

zig translate-c .\main.c -lc > .\main.zig

Тут сразу стоит указать, что конвертер принимает только один файл для конвертации за раз. То есть нельзя весь проект на языке C разом перевести на язык Zig с соблюдением всех тонкостей проекта, иерархии папок, и всего такого, просто кинув в конвертер путь до папки. Не умеет он так. Да и, если подумать, то это будет очень проблематично реализовывать. Поэтому есть ограничение. Но даже конвертации одного файла за раз вполне хватает. Биндинги, там, удобно делать, вот это вот всё.

После конвертации код из 5 строк превратиться в 1322 строки. И если у тебя, дорогой читатель, возник вопрос «откуда 1322 строки», то ответ прост. В примере выше в коде языка C кроме функции main есть еще директива include указывающая на заголовочный файл stdio.h, где располагается функция printf, которая, собственно, нужна для передачи данных в стандартный вывод. И этот заголовочный файл с собой несёт ещё заголовочные файлы, и они, в свою очередь, тоже. Компилятор языка Zig транслирует весь код на C, который располагается в единице трансляции. И для обработки некоторых нюансов языка C при конвертации его в язык Zig, конвертер выполняет работу препроцессора. Здесь есть некоторая логика. О чём я упомяну ниже. И на сколько я понимаю, при конвертации кода языка C в язык Zig на разных операционных системах будет разное количество строк на выходе, так как реализация стандартной библиотеки для разных платформ отличается.

Если поискать среди этих 1322 строк, то можно найти нужную функцию main.

pub export fn main() c_int {
    _ = printf("Hello, world!\n");
    return 0;
}

В принципе, выглядит так же, как на языке C. Это тоже объяснимо, так как язык Zig тесно связан с языком C. Мемная КДПВ отражает отношения.

И здесь стоит упомянуть нюанс. Если поискать функцию printf, то можно найти код с комментарием:

pub extern fn printf(__format: [*c]const u8, ...) c_int; // ...\zig\current\lib\libc\include\any-windows-any/stdio.h:396:5: warning: TODO unable to translate variadic function, demoted to extern

Комментарий говорит сам за себя. Вместо троеточия в начале комментария был ещё путь, я его убрал, так как было слишком длинно. Для тех, кто не владеет английским, переведу, как сам понимаю:

не может сконвертировать вариативную функцию, представлена как «внешняя»

Конвертер не стал конвертировать функцию, а лишь написал объявление функции с ключевым словом extern, что означает, что сама функция отсутствует в этой единице трансляции, и описана где-то в другом месте. То же самое с директивами define (не всеми, простые он превращает в константы). То есть компилятор Zig не умеет конвертировать некоторый синтаксис языка C, так как в языке Zig нет аналогов. И оставляет комментарии, с которыми программисту придётся разбираться самому. В этом поведение есть логика. Именно эти возможности языка С создают больше всего проблем (если не учитывать проблемы с указателями), которые труднее всего находить и исправлять. Именно от этих возможностей Эндрю Келли, автор языка Zig отказался в первую очередь. И я абсолютно с ним согласен. Директива define, например, штука простая, элегантная и в тоже время крайне опасная. Что неоднократно мной же подтверждалось из практики. А я ведь C++ программист. В C++ define тоже любят. И у define там те же проблемы.

Резюмирую. Конвертер не панацея. Он лишь немного упрощает переход от языка C на язык Zig, если конечно такое необходимо. Как я упоминал выше, с помощью него удобно создавать биндинги для библиотек.

И другое...

Про менеджер пакетов я упомяну далее в статье. А про линкер нечего написать, так как я про это не знаю. Ну в том смысле, что работает и мне этого пока достаточно. Ещё разбираюсь, что к чему. Вроде как сейчас используется LLD, что логично, ноги компилятора Zig растут из LLVM, но вроде как хотят сделать отдельную команду для компилятора, чтобы он мог выступать и линкером для внешних систем сборки. Короче, stay tuned.

Глава 2. Изменения системы сборки Zig в версии 0.11.0

Сложно правильно начать эту главу, попробую так.

Язык всё ещё на пути своего становления, из-за чего в разных проектах в интернете можно встретить код для разных версии языка Zig, которые между собой имеют различия и даже могут быть частично несовместимы. Связано это с тем, что все самые новые возможности языка Zig всегда первыми появляются в master ветке его основного репозитория. Что логично. А уже потом они переходят в стабильные релизы, а далее по проектам. И между минорными версиями стабильных релизов бывают временные промежутки, когда в master ветке появилась «фитча», которая очень нужна в проектах уже сейчас, а ждать следующую минорную версию стабильного релиза языка у авторов этих проектов нет желания. Именно поэтому бывают проекты, где авторы переходили в master ветку кода, хотя до этого пользовались стабильным релизом. И после выхода стабильного релиза возвращались к нему (или не возвращались). Обычно (и это считается признаком хорошего тона) авторы проектов указывают какую версию кода они используют в данный момент, чтобы пользователь понимал, сможет ли он использовать код проекта, или придётся доработать его под ту версию языка, которую использует сам пользователь, так как для многих операционных систем с открытым исходным кодом в их репозиториях контрибьюторы обычно размещают только стабильные релизы программ, а значит пользователям доступны именно стабильные версии языка Zig.

Стоит упомянуть, что были даже войны правок, когда одна часть контрибьюторов проекта использовали стабильные релизы языка Zig, чтобы поддерживать совместимость кода для большинства пользователей, а другая часть контрибьюторов в погоне за нововведениями переписывали код под самые последние версии из master ветки, и это ломало совместимость.

Поэтому версию 0.11.0 ждали, чтобы стабилизировать ряд нововведений, которые ранее были только в master ветке. И надеюсь, что дальше проектов использующих стабильные релизы будет больше. Потому что переключение между разными версиями языка несколько затрудняет поддержку кода. И война правок на самом деле утомляет. Я сам приверженец только стабильных релизов.

И так. О нововведениях...

Встроенный менеджер пакетов

Первое, что стоит указать.

Ранее у Zig были сторонние менеджеры пакетов (zigmod, gyro), и были они вполне рабочие. Но удобство использования ими было спорным. Добавление менеджера пакетов внутрь системы сборки языка Zig решало сразу несколько проблем. Первая решённая проблема, которую стоит упомянуть - это поддержка актуальности кода менеджера пакетов. Теперь нет зависимости от сторонних проектов, а значит функционал будет всегда актуален. Вторая решённая проблема - универсальность пакетов для компилятора, а стало быть для конечного пользователя. Сторонние менеджеры пакетов были несовместимы между собой. При этом для использования менеджера пакетов требовалось использовать последнюю версию языка из master ветки репозитория, что могли себе позволить не все. И сам сторонний менеджер пакетов выступал, как отдельный пакет для проекта (эдакий пакет с пакетами). Возможно одна из следующих статей будет связана с описанием встроенного менеджера пакетов.

Вместо Pkg теперь Module

Добавление менеджера пакетов потребовало переосмыслить название блока кода Pkg, который характеризовал собой отдельный исходный код, что-то типа библиотеки на языке Zig, не являющейся частью всего проекта. По сути это были привычные для разработчиков других языков «модули». Поэтому был сделан рефакторинг, чтобы отделить сущности пакетов для менеджера пакетов и пакетов (модулей) для кода. Для простоты, если ты, дорогой читатель, встретишь в интернете Pkg блоки в файле build.zig, то это означает, что код проекта был написан до версии 0.11.0, и точно потребуется рефакторинг, чтобы использовать код проекта у себя.

Применение структур для настроек

Этот процесс идёт уже давно. Поэтому новостью он для кого-то не будет.

До версии 0.11.0 многие настройки компиляции в файле build.zig проводились старым «дедовским» способом: функция принимала отдельные параметры для каждой конкретного настройки компиляции. Это означало, что некоторые функции принимали столько параметров, сколько функции нужно было для работы, если нужны были дополнительные настройки компиляции, то вызывались дополнительные функции. Происходит переосмысление этого подхода, так как удобство структур неоспоримо. Во многих местах внесли изменения, и теперь вместо передачи нескольких параметров в функции, передаётся структура настроек для конкретного блока компиляции. Примеры из документации, код создания исполняемого файла в версии 0.10.1:

const exe = b.addExecutable("example", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();

Он же, но в версии 0.11.0

const exe = b.addExecutable(.{
    .name = "example",
    .root_source_file = .{ .path = "src/main.zig" },
    .target = target,
    .optimize = optimize,
});
// exe.install(); <- это больше не работает
b.installArtifact(exe); <- теперь это работает

upd (включение из телеграмм чата): оказалось, что в примере кода в документации версии 0.11.0 ошибка. конструкция exe.install(); официально удалена.

Мне лично второй подход нравится больше. Я сам стал замечать, что чаще думаю об таких же вариантах реализации в своем коде.

Глава 3. Сборка кода на языке C

И вот теперь можно перейти к главной теме статьи. Пример компиляции кода языка C через компилятор языка Zig напрямую я уже показал выше. В этой главе речь пойдёт об использовании файла build.zig. И примером будет эксперимент с библиотекой raylib. Я уже упоминал об эксперименте в предыдущей статье. Но там код отличается, так как в том эксперименте я писал биндинги raylib для языка Zig. В этом примере будет только сборка библиотеки и её использование в коде языка Zig.

У библиотеки raylib есть официальный файл сборки build.zig в GitHub репозитории для тех, кто программирует на языке Zig. Но в определённый момент времени он попал под войны правок. И поэтому те, кто пользовался стабильными релизами ( например, такие как я) столкнулись с проблемами сборки новых релизов самой библиотеки. И для решения это проблемы в определенный момент был написан свой вариант файл build.zig, чтобы не зависеть от официальной реализации.

В этот файл можно подсмотреть за списком всех библиотек необходимых для сборки библиотеки raylib для других платформ.

Для статьи, собственно, мне понадобится сама библиотека raylib. Файлы исходного кода склонировал из GitHub репозитория в папку сгенерированного мной ранее шаблона для исполняемого приложения.

И так, сначала общее...

Файл build.zig

Ниже приведён листинг файла build.zig из сгенерированного шаблона. Комментарии не стал удалять.

const std = @import("std");

// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});

    // Standard optimization options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
    // set a preferred release mode, allowing the user to decide how to optimize.
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "test-exe",
        // In this case the main source file is merely a path, however, in more
        // complicated build scripts, this could be a generated file.
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    // This declares intent for the executable to be installed into the
    // standard location when the user invokes the "install" step (the default
    // step when running `zig build`).
    b.installArtifact(exe);

    // This *creates* a Run step in the build graph, to be executed when another
    // step is evaluated that depends on it. The next line below will establish
    // such a dependency.
    const run_cmd = b.addRunArtifact(exe);

    // By making the run step depend on the install step, it will be run from the
    // installation directory rather than directly from within the cache directory.
    // This is not necessary, however, if the application depends on other installed
    // files, this ensures they will be present and in the expected location.
    run_cmd.step.dependOn(b.getInstallStep());

    // This allows the user to pass arguments to the application in the build
    // command itself, like this: `zig build run -- arg1 arg2 etc`
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    // This creates a build step. It will be visible in the `zig build --help` menu,
    // and can be selected like this: `zig build run`
    // This will evaluate the `run` step rather than the default, which is "install".
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    // Creates a step for unit testing. This only builds the test executable
    // but does not run it.
    const unit_tests = b.addTest(.{
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    const run_unit_tests = b.addRunArtifact(unit_tests);

    // Similar to creating the run step earlier, this exposes a `test` step to
    // the `zig build --help` menu, providing a way for the user to request
    // running the unit tests.
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_unit_tests.step);
}

В какой-то мере похоже на скрипты некоторых систем сборки. Но отличие в том, что внутри файла build.zig можно писать полноценный код на языке Zig, который будет собран перед компиляцией основного кода. Но воспользоваться этим кодом можно только для помощи компиляции. То есть в итоговый файл проекта этот код не попадёт.

Никто не запрещает, конечно, добавить функцию main и воспользоваться командой zig build-exe build.zig, чтобы собрать исполняемое приложение. Но какой в этом смысл?

Смысл файла build.zig в том, что программисту разрабатывающего на языке Zig не нужно использовать сторонние системы сборки для полноценной сборки проекта, в отличии от языка C. Достаточно лишь воспользоваться компилятором. Примеры команд я демонстрировал выше. И при этом программисту не нужно учить дополнительные языки, которые могут использоваться в сторонних системах сборки для написания скриптов.

Такой же подход есть и в ряде других языков, в частности в Rust, откуда Эндрю Келли, автор языка Zig подрезал идею. Но в отличии от авторов Rust, авторы Zig не стали прятать скрипт системы сборки за абстракциями. Хорошо это, или плохо вопрос неоднозначный. Мне лично нравится такой подход. Но в тоже время, мне нравится и подход в Rust и Go. Может быть в будущем произойдут изменения в системе сборки языка Zig, который приведут к скрытию файла скрипта, но пока авторы языка не рассматривают такой необходимости. Я уже делал личный комментарий по этому поводу, и снова его повторю. Я считаю это лучшей реализацией.

В общем, вся магия сборки настраивается в файле build.zig. Здесь же добавляются необходимые этапы сборки. А так как я собираю библиотеку raylib, то к ней и приступлю.

Создание статической библиотеки

Мне нужна статическая библиотека, поэтому я вызываю функцию addStaticLibrary у основной структуры b, отвечающей за настройку сборки.

const raylib = b.addStaticLibrary(.{
    .name = "raylib",
    .target = target,
    .optimize = optimize,
});

Функция addStaticLibrary возвращает структуру Step.Compile, характеризующую шаг компиляции. По сути addExecutable, addStaticLibrary, addSharedLibrary создают структуру одного типа. А так как в Zig нет полиморфизма, то это может выглядеть как дублирование кода. Но не совсем так. Такое разделение сделано нарочно. Всё для наглядности, а общая структура Step.Compile необходима для удобства связывания отдельных шагов в последовательности.

Далее нужно добавить файлы с исходным кодом библиотеки Raylib.

Добавление файлов с кодом на языке C

Для добавления исходного кода на языке C я вызываю функцию addCSourceFiles у созданного ранее шага компиляции raylib.

raylib.addCSourceFiles(
    &.{
        raylib_srcdir ++ "/raudio.c",
        raylib_srcdir ++ "/rcore.c",
        raylib_srcdir ++ "/rmodels.c",
        raylib_srcdir ++ "/rshapes.c",
        raylib_srcdir ++ "/rtext.c",
        raylib_srcdir ++ "/rtextures.c",
        raylib_srcdir ++ "/utils.c",
        raylib_srcdir ++ "/rglfw.c",
    },
    &.{
        "-std=gnu99",
        "-D_GNU_SOURCE",
        "-DGL_SILENCE_DEPRECATION=199309L",
        // https://github.com/raysan5/raylib/issues/1891
        "-fno-sanitize=undefined",
    }
);

В функцию addCSourceFiles передаётся два массива строк:

  • массив строк с путями до файлов с исходным кодом;

  • массив строк с флагам компиляции.

Переменная raylib_srcdir хранит путь до папки с файлами исходного кода библиотеки raylib. Сделано это для удобства. Код ниже генерирует строку пути во время компиляции. Код написан не мной, я лишь разместил объявление.

const root_dir = struct {
    fn getSrcDir() []const u8 {
        return std.fs.path.dirname(@src().file) orelse ".";
    }
}.getSrcDir();

const raylib_srcdir = root_dir ++ "/raylib/src";

Теперь нужно не забыть добавить заголовочных файлов. Всё же с кодом на языке C имею дело.

Добавление путей до папок с заголовочными файлами

Для добавления в шаг компиляции путей до папок с заголовочными файлами я вызываю функцию addIncludePath. И добавляю два пути с заголовочными файлами необходимыми для библиотеки raylib.

raylib.addIncludePath(.{ .path = raylib_srcdir ++ "/external/glfw/include" });
raylib.addIncludePath(.{ .path = raylib_srcdir ++ "/external/glfw/deps/mingw" });

Функцию необходимо вызывать для каждой папки отдельно. С одной стороны жаль, что нельзя пути пачкой забросить, прям просится здесь такой функционал, а с другой - смотрю на это, и как-то meh, и так норм.

Библиотека написана на языке C и зависит от стандартной библиотеки языка C. Поэтому необходимо её прилинковать.

Линкуем стандартную библиотеку языка C

Для линковки стандартной библиотеки языка C к шагу компиляции я вызываю функцию linkLibC.

raylib.linkLibC();

Функция linkLibC не принимает никаких параметров.

Библиотека raylib зависит от ряда других библиотек, их набор зависит от платформы, для которой нужно скомпилировать код, поэтому необходимые библиотеки нужно дополнительно прилинковать.

Линкуем системные библиотеки

Для линковки системных библиотек к шагу компиляции я вызываю функцию linkSystemLibrary, в которую передаю название библиотеки. Для операционной системы Window мне нужны три библиотеки: winmm, gdi32, opengl32. Итоговый код:

raylib.linkSystemLibrary("winmm");
raylib.linkSystemLibrary("gdi32");
raylib.linkSystemLibrary("opengl32");

Аналогичные ощущения, что и с путями до заголовочных файлов. Но тут стоит сделать ремарку. В стандартной библиотеке языка Zig есть еще одна функция для линковки системных библиотек - linkSystemLibrary2, которая принимает структуру с дополнительными опциями, и эта же функция вызывается внутри функции linkSystemLibrary. То есть функция linkSystemLibrary сейчас обёртка над функцией linkSystemLibrary2, и возможно в будущем будут изменения.

И осталась одна вещь связанная с библиотекой raylib - определить для самой библиотеки для какой платформы она собирается.

Добавляем директиву #define

Выбор платформы для сборки библиотеки raylib определяется через макрос. Всего макросов пять: PLATFORM_DESKTOP, PLATFORM_ANDROID, PLATFORM_RPI, PLATFORM_DRM и PLATFORM_WEB. Для всех стационарных операционных систем, таких как Windows, Linux, MacOS, *BSD, нужно определить макрос PLATFORM_DESKTOP.

Чтобы определить макрос для C кода в шаге компиляции я вызываю функцию defineCMacro. И передаю имя макроса первым параметром.

raylib.defineCMacro("PLATFORM_DESKTOP", null);

Второй параметр функции defineCMacro - это вторая часть макроса, или его значение, которое мне не нужно указывать, поэтому туда передаю null.

На этом подготовка шага компиляции для библиотеки raylib закончена. Теперь нужно добавить путь к папке с залоговочными файлами библиотеки raylib и саму библиотеку к шагу компиляции исполняемого приложения.

Добавление путей до папок с заголовочными файлами 2

Чтобы можно было использовать элементы библиотеки raylib нужно предоставить путь до папки с заголовочными файлами библиотеки raylib. Это делается для упрощения, чтобы можно было использовать относительные пути внутри кода на языке Zig. Для этого вызываю функцию addIncludePath шага компиляции исполняемого приложения. И передаю ему путь до папки с исходным кодом библиотеки raylib.

exe.addIncludePath(.{ .path = raylib_srcdir });

«Линкуем» ранее «созданную» статическую библиотеку

Через функцию linkLibrary я шагу компиляции исполняемого приложения указываю, что к готовому объектному файлу нужно прилинковать готовую статическую библиотеку raylib.

exe.linkLibrary(raylib);

И на этом всё. Возможно у тебя, дорогой читатель, есть вопрос, а куда класть, ложить, или вставлять весь тот код шага для сборки библиотеки raylib, который я написал выше? Я его разместил сразу после создания шага исполняемого приложения, кроме кода генерирующего raylib_srcdir. Его я разместил перед функцией build.

Листинг файла build.zig со всеми изменениями вместе

const std = @import("std");

const root_dir = struct {
    fn getSrcDir() []const u8 {
        return std.fs.path.dirname(@src().file) orelse ".";
    }
}.getSrcDir();

const raylib_srcdir = root_dir ++ "/raylib/src";

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

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

    const raylib = b.addStaticLibrary(.{
        .name = "raylib",
        .target = target,
        .optimize = optimize,
    });

    raylib.addCSourceFiles(&.{
        raylib_srcdir ++ "/raudio.c",
        raylib_srcdir ++ "/rcore.c",
        raylib_srcdir ++ "/rmodels.c",
        raylib_srcdir ++ "/rshapes.c",
        raylib_srcdir ++ "/rtext.c",
        raylib_srcdir ++ "/rtextures.c",
        raylib_srcdir ++ "/utils.c",
        raylib_srcdir ++ "/rglfw.c",
    }, &.{
        "-std=gnu99",
        "-D_GNU_SOURCE",
        "-DGL_SILENCE_DEPRECATION=199309L",
        // https://github.com/raysan5/raylib/issues/1891
        "-fno-sanitize=undefined",
    });

    raylib.addIncludePath(.{ .path = raylib_srcdir ++ "/external/glfw/include" });
    raylib.addIncludePath(.{ .path = raylib_srcdir ++ "/external/glfw/deps/mingw" });

    raylib.linkLibC();
    raylib.linkSystemLibrary("winmm");
    raylib.linkSystemLibrary("gdi32");
    raylib.linkSystemLibrary("opengl32");

    raylib.defineCMacro("PLATFORM_DESKTOP", null);

    exe.addIncludePath(.{ .path = raylib_srcdir });
    exe.linkLibrary(raylib);

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);

    run_cmd.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

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

Глава 4. Используем код на языке C

В этой главе я продемонстрирую как можно использовать код языка C в проекте на языке Zig, на примере подготовленной библиотеки raylib. И начну я c...

Импорт заголовочных файлов языка C

Для работы с кодом языка C в языке Zig есть ряд встроенных функций. Мне же для демонстрации понадобятся только две: cImport и cInclude.

const raylib = @cImport({
    @cInclude("raylib.h");
});

Теперь всё, что доступно в заголовочном файле raylib.h можно использовать через псевдоним raylib.

Пример использования кода C

Для статьи я сконвертировал самый первый пример с официальной страницы примеров библиотеки raylib.

const std = @import("std");

const raylib = @cImport({
    @cInclude("raylib.h");
});

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

    raylib.InitWindow(
        screen_width,
        screen_height,
        "raylib. Хабр: Варим C с компилятором Zig и его build.zig",
    );

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

    while (!raylib.WindowShouldClose()) {
        raylib.BeginDrawing();
        raylib.ClearBackground(raylib.RAYWHITE);
        raylib.DrawText("Hello, Habr!", 190, 200, 48, raylib.GRAY);
        raylib.EndDrawing();
    }

    raylib.CloseWindow();
}

Им заменил код в файле main.zig. Выполнил в терминале (консоли) команду zig build run, чтобы сразу запустить готовое приложение. В итоге получил результат.

Окно с надписью «Hello, Habr!»
Окно с надписью «Hello, Habr!»

Эпилог

Эта статья базовый пример того, как можно использовать код на языке C в проектах на языке Zig. Я хотел показать, что работа с кодом написанном на языке C в проекте на языке Zig на самом деле несложная в отличии от ряда других языков. И что язык Zig был специально разработан с учётом тесной взаимосвязи с языком C. О чём неоднократно упоминал сам автор языка Zig, Эндрю Келли. И именно эта особенность языка Zig понравилась другим программистам, которые стали экспериментировать с языком Zig и в итоге стали помогать его развивать.

Может показаться, что многое в статье описано поверхностно. И ты, дорогой читатель, будешь прав. Упрощая себе задачу я старался не углубляться в тонкости и нюансы. Указывал только те нюансы, что были, как мне кажется, важны для статьи. Я боялся, что если я начну углубляться в детали, мне самому станет скучно дописывать статью. Потому что каждый аспект как компилятора, так и языка Zig, затронутые мной в этой статье можно раскрывать детально долго и очень скучно. Потому что без учёта контекста статьи каждая деталь важна. И в некоторых местах очень много деталей. Если бы я осветил каждую из них, то у тебя, дорогой читатель, были вопросы к тому, почему заголовок статьи не соответствует наполнению. То есть я стараслся придерживаться той темы, которую хотел осветить. Я постараюсь в возможных будущих статьях специально подбирать темы, в которых буду затрагивать конкретные аспекты языка Zig, чтобы раскрывать их более детально. На самом деле есть о чём написать.

На этом всё. Спасибо за внимание!


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

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

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

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

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

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

Сайт для обучения ziglearn только на английском
Сайт для обучения zighelp английский и русский (на очень ранней стадии)
Ziglings: обучение через решение проблем
Набор задачек на Exercism

Страничка Zig на rosettacode c примерами кода (upd от учасника @forthuse)
Zig By Example - примеры кода на Zig
(Примеры простенькие, и рекомендуется для начала поизучать сам язык, так как комментариев к коду в примерах нет)

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

Комментарии (28)


  1. FelixTheMagnificent
    25.08.2023 19:20

    Это все замечательно, а можно хоть немного про язык, плюсы/минусы, сравнение, применимость?

    Каждый раз, читая про какой-то новый язык, становится понятно, как скомпилировать приложение, но совершенно не понятно, для чего вообще он.


    1. forthuse
      25.08.2023 19:20
      +2

      ? Может какие то ответы есть в немногочисленных статьях в
      Хабе по Zig на Habr


      1. FelixTheMagnificent
        25.08.2023 19:20
        +1

        Вот все равно не понял. Не знаю, может я тупой, но есть 3 языка: Go, Rust, C/C++, теперь еще Zig, и мне не понятно главное: в чем разница-то?

        Отличия между C/C++ и Rust хотя бы очевидны: оба достаточно низкоуровневые, но при этом возможностей отстрелить себе ногу в Rust в разы меньше. Отличия между этими двумя языками и Go - Go еще и интерпретируемый.

        А Zig как-то в сторонке держится: мы можем транслировать код с С/С++ на Zig, а какие преимущества/недостатки в сравнении - все еще непонятно.


        1. apro
          25.08.2023 19:20

          Отличия между этими двумя языками и Go - Go еще и интерпретируемый.

          Go компилируемый, как и Rust с C/C++. Главное отличие Go - это сборка мусора, утиная типизация и его подход к многозадачности.


        1. AnimeSlave Автор
          25.08.2023 19:20

          есть 3 языка: Go, Rust, C++

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

          У Zig же уникальная ситуация. Он может именно заменить язык C. Я этой статьёй это показал, что это возможно, и это просто. И при этом сам язык предоставляет дополнительные возможности, внедрение которых ничего не стоит, потому что они часть языка. Язык Zig во многом повторяет поведение языка C, что даёт возможность получить при переходе между языками аналогичное поведение кода после компиляции. Тут есть о чём ещё упомянуть, но я не смогу покрыть всё своими текущими знаниями


        1. Kelbon
          25.08.2023 19:20
          -2

          Давайте посчитаем вместе:
          1. Go
          2. Rust
          3. C
          4. C++
          Это 4 языка.

          Во вторых в расте столько же точек отстрела + свои ещё, в подробности лучше не вдаваться


          1. Helltraitor
            25.08.2023 19:20
            +1

            Ну-ка, давайте вдаваться. А не то ваш комментарий как нонсенс выглядит


            1. Kelbon
              25.08.2023 19:20

              полного списка UB в расте вы нигде не найдёте, модель памяти раста не определена и примерная цитата из его "библии" "пока используется модель памяти С++, когда то потом изменим", в добавление к этому есть УБ связанные именно с растом, например если у вас существует 2 мутабельных ссылки на один объект, то это УБ в расте


              1. AnimeSlave Автор
                25.08.2023 19:20

                ...модель памяти раста не определена и примерная цитата из его "библии" "пока используется модель памяти С++...

                Вот это одно из тех вещей, что в Rust мне непонятно. Особенно с учётом того, что Rust существует уже много лет, и метит на место языка C в межпрограммном «общении». Странно это. ABI до сих пор не устаканилось. Оно конечно не мешает писать программы, но осадочек остаётся

                полного списка UB в расте вы нигде не найдёте...

                А вот тут стоит сделать поправку. Списка UB в Rust и не может быть, так как UB подразумевает отсутствие описания поведения в стандарте, и реализация поведения ложится на плечи компилятора, а так как у Rust разработчики языка и компилятора это одна группа людей (то есть одна компания), то они просто не могут знать, что у них UB на самом деле. То есть не как в C или C++, где есть комитет по стандарту, который формирует стандарт и отдельные разработчики компиляторов (коих десятки), которые этот стандарт интерпретируют. И есть места, которые стандарт просто не может охватить, так как эти места по настоящему зависят от конкретной реализации. То есть в случае C и C++ список UB составить можно. Но то, что Rust имеет свои подводные камни, это факт


              1. Helltraitor
                25.08.2023 19:20
                -3

                Причем тут модель памяти в Rust? Речь же про неопределенное поведение.

                В Rust не может существовать две мутабельные ссылки на один объект, но могут быть два мутабельных указателя на один объект.

                полного списка UB в расте вы нигде не найдёте

                Дарю: https://doc.rust-lang.org/reference/behavior-considered-undefined.html

                Если мы говорим про UB, то нужно делить Rust на safe и unsafe. UB в safe не существует. Список вещей, которые считаются UB предоставлен.

                Во вторых в расте столько же точек отстрела + свои ещё

                Как хорошо, что это неправда

                Я так понимаю, на этом мы заканчиваем


                1. Kelbon
                  25.08.2023 19:20
                  -1

                  По вашей же ссылке очень большими красными буквами написано, что список неполный и вообще никто не знает что можно, а что нет


                  1. Helltraitor
                    25.08.2023 19:20

                    Не заметил, на самом деле, но если есть что ДОБАВИТЬ - прошу


    1. AnimeSlave Автор
      25.08.2023 19:20

      Вопрос хороший, вот ответ на него уже будет не очень. Проблема здесь в самом вопросе. Эту статью я писал на определенную тему, так как задумал её ещё при написании предыдущей статьи, и старался придерживаться темы. Потому что мне самому понравился такой ход разработчиков языка - упростить взаимодействие с языком C. И в эпилоге я написал почему статья такая.

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


      1. lieff
        25.08.2023 19:20

        Если нацелен на замену, хорошо бы бенчмарки посмотреть, есть такие?


        1. AnimeSlave Автор
          25.08.2023 19:20

          Моих личных нет. Есть сайт, но насколько можно ему доверять не знаю, пару раз встречал его в разных статьях


  1. forthuse
    25.08.2023 19:20
    +1

    1. forthuse
      25.08.2023 19:20

      Интересно, что и много вариантов написания Форт (Forth) на Zig
      Находится на Github :)


      P.S. Правда не понятно чем он "лучше" для этого действия подходит, в сравнении с разными реализациями Форт на других высокоуровневых языках помимо ассемблера.
      К, примеру, и на Rust есть несколько реализаций Форт.


      Кстати, а Online компиляторы Zig какие наиболее актуальны учитывая "нестабильность" языка?


      1. Zhuikoff
        25.08.2023 19:20
        +1

        Есть древнее поверье, что если ты не написал свою реализацию Forth, ты не настоящий программист.


    1. AnimeSlave Автор
      25.08.2023 19:20

      Спасибо. Забыл про rosettacode. У самого в закладках лежала, и забыл про неё


  1. klopp_spb
    25.08.2023 19:20

    Я так и не понял, зачем мне переходить с C на что-то ещё (в этой нише).

    там владелец чата странно себя ведёт

    А...


    1. AnimeSlave Автор
      25.08.2023 19:20

      Я так и не понял, зачем мне переходить с C на что-то ещё

      Простите, что вопросом на вопрос отвечаю. А вы хотите перейти?


      1. klopp_spb
        25.08.2023 19:20
        +1

        Неважно что хочу я. Важно что требуют задачи. Если появятся такие, для которых Zig окажется эффективней - почему бы и нет? Но пока их не вижу.


        1. AnimeSlave Автор
          25.08.2023 19:20
          -1

          На самом деле очень важно, что именно вы хотите. Так как вы лично решаете перейти на новый язык или нет. Я могу ошибаться, но предполагаю, что вы не прогаммист на C, так как проблемы языка C очевидны и нельзя их не замечать


          1. klopp_spb
            25.08.2023 19:20

            Пишу на C с 1992 года.


            1. AnimeSlave Автор
              25.08.2023 19:20

              Охотно верю


          1. le2
            25.08.2023 19:20
            -1

            мир иначе устроен. Когда вы лопату меняете на экскаватор, то получаете кучу проблем: требуется обучение и права, солярка, ремонт, расходники и так далее.
            Фундаментально Си "божественен" и неулучшаем. Потому что случилась цепочка событий: Multics -> Unix -> Posix
            То есть у человечества есть единый стандарт на ось (Posix) и почти все операционки кроме Винды этому соответствует. И C89 там прибит гвоздями как язык ядра. Подавляющее большинство языков вначале транслируются на Си, то есть не являются самостоятельными.
            Торвальдс начал процедуру миграции на С99, но что-то другое никому не надо. Достоинство Си в том что очень простой компилятор, который очень дешево портировать на любую железку. Ядро собирается без всяких зависимостей, без всех библиотек.
            Второй важнейший язык это Си++ он также прибит гвоздями и устанавливается в самом начале построения дистрибутива. Мистер Степанов годами ходил за Стауструпом, пока не заставил прикрутить к языку шаблоны (STL). Си++ остается нижнем уровнем для прикладного кода.
            Итого, пока существует эта парадигма POSIX то остаются эти два важнейших языка. Всё остальное является факультативными паразитическими сущностями. И так будет оставаться пока парадигма компьютера не сменится на некую биологическую или квантовую архитектуру.


            1. AnimeSlave Автор
              25.08.2023 19:20
              +1

              Застоялось взглядов. Это мне знакомо.

              Я отвечу так. Я понимаю о чём вы пишете. И понимаю, что в некоторых местах вы ошибаетесь. Проблема языка C не в том, что он не улучшаем, а в том, что комитет по стандартизации его не хочет улучшать. Я не утверждаю, что улучшений вообще нет. Нет тех улучшений, которые могут нивелировать недостатки накопленные за года. Здесь работает человеческий фактор. И понятно почему. Многое завязано на «стабильность» языка C. Для комитета это практически «бизнес». Для некоторых представителей комитета это и вправду бизнес. Программистам просто приходится мириться с тем, что сейчас представляет из себя язык C. А его есть куда улучшать. Даже упомянутый вами Товальдс говорил, что он готов принимать в ядро код написанный на других языках, если это будет «достойный» язык. Rust же он разрешил (с ограничениями). А Rust в сравнении с C куда более «сложный» язык.

              Про остальное я не буду комментировать, там слишком холиварно может получиться


  1. klopp_spb
    25.08.2023 19:20

    del