Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Вводное слово
По случаю выхода версии 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();
Мне лично второй подход нравится больше. Я сам стал замечать, что чаще думаю об таких же вариантах реализации в своем коде.
Глава 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
, чтобы сразу запустить готовое приложение. в итоге получил результат.
Эпилог
Эта статья базовый пример того, как можно использовать код на языке C в проектах на языке Zig. Я хотел показать, что работа с кодом написанном на языке C в проекте на языке Zig на самом деле несложная в отличии от ряда других языков. И что язык Zig был специально разработан с учётом тесной взаимосвязи с языком C. О чём неоднократно упоминал сам автор языка Zig, Эндрю Келли. И именно эта особенность языка Zig понравилась другим программистам, которые стали экспериментировать с языком Zig и в итоге стали помогать его развивать.
Может показаться, что многое в статье описано поверхностно. И ты, дорогой читатель, будешь прав. Упрощая себе задачу я старался не углубляться в тонкости и нюансы. Указывал только те нюансы, что были, как мне кажется, важны для статьи. Я боялся, что если я начну углубляться в детали, мне самому станет скучно дописывать статью. Потому что каждый аспект как компилятора, так и языка Zig, затронутые мной в этой статье можно раскрывать детально долго и очень скучно. Потому что без учёта контекста статьи каждая деталь важна. И в некоторых местах очень много деталей. Если бы я осветил каждую из них, то у тебя, дорогой читатель, были вопросы к тому, почему заголовок статьи не соответствует наполнению. То есть я стараслся придерживаться той темы, которую хотел осветить. Я постараюсь в возможных будущих статьях специально подбирать темы, в которых буду затрагивать конкретные аспекты языка 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 реддита.