Оскільки ми вже охопили більшу частину мови, завершуватимемо, переглянувши кілька тем і розглянувши ще кілька практичних аспектів використання Zig. При цьому ми зробимо більший огляд стандартної бібліотеки і покажемо менш тривіальні фрагменти коду.
Знову «Висячі» вказівники
Ми починаємо з розгляду інших прикладів висячих вказівників. Це може здатися дивною річчю, на якій варто зосередитися, але якщо ви прийшли з мови зі збиранням сміття, це, мабуть, найбільша проблема, з якою ви зіткнетеся.
Чи можете ви розібратися, що дає такі результати?
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var lookup = std.StringHashMap(User).init(allocator);
defer lookup.deinit();
const goku = User{.power = 9001};
try lookup.put("Goku", goku);
// повертає опціональне значення; .? запанікує, якщо "Goku"
// немає в хеш-мапі
const entry = lookup.getPtr("Goku").?;
std.debug.print("Сила Goku становить: {d}\n", .{entry.power});
// повертає true/false залежно від того, чи був елемент видалений
_ = lookup.remove("Goku");
std.debug.print("Сила Goku становить: {d}\n", .{entry.power});
}
const User = struct {
power: i32,
};
Коли я це запустив, я отримав
Сила Goku становить: 9001
Сила Goku становить: -1431655766
Цей код демонструє узагальнену структуру std.StringHashMap зі стандартної бібліотеки Zig, яка є спеціалізованою версією std.AutoHashMap з типом ключа, встановленим як []const u8. Навіть якщо ви не на 100% впевнені, що відбувається, можна припустити, що мій вивід пов’язаний із тим, що наш другий print виконується після того, як ми вилучаємо (remove) запис із lookup. Закоментуйте виклик remove, і результат буде нормальним.
Ключ до розуміння наведеного вище коду — знати, де існують дані/пам’ять, або, іншими словами, хто ними володіє. Пам’ятайте, що аргументи Zig передаються за значенням, тобто ми передаємо [поверхневу] копію значення. User у нашому lookup — це не та сама пам’ять, на яку посилається goku. Наш наведений вище код має двох користувачів, кожен зі своїм власником. goku належить main, а його копія належить lookup.
Метод getPtr повертає вказівник на значення в мапі — у нашому випадку він повертає *User. Ось у чому полягає проблема: remove робить наш вказівник entry недійсним. У цьому прикладі близькість getPtr і remove робить проблему дещо очевидною. Але неважко уявити код, що викликає remove, не знаючи, що посилання на запис міститься десь в іншому місці.
Коли я писав цей приклад, я не був упевнений, що станеться. Можна було б реалізувати
removeчерез встановлення внутрішнього прапора, відкладаючи фактичне видалення до наступної події. Якби це було так, наведене вище могло б «спрацювати» в простих випадках, але дало б збій у складніших. Це звучить жахливо важко налагоджувати.
Окрім того, щоб не викликати remove, ми можемо виправити це кількома різними способами. По-перше, ми можемо використовувати get замість getPtr. Це повертатиме User замість *User і таким чином — копію значення з lookup. Тоді у нас буде три User:
- Наш оригінальний
goku, прив’язаний до функції. - Копія в
lookup, яка належить пошуку. - І копія нашої копії,
entry, також прив’язана до функції.
Оскільки entry тепер буде власною незалежною копією користувача, видалення значення з lookup не зробить її недійсною.
Інший варіант — змінити тип пошуку з StringHashMap(User) на StringHashMap(*const User). Цей код працює:
learning.zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// User -> *const User
var lookup = std.StringHashMap(*const User).init(allocator);
defer lookup.deinit();
const goku = User{.power = 9001};
// goku -> &goku
try lookup.put("Goku", &goku);
// getPtr -> get
const entry = lookup.get("Goku").?;
std.debug.print("Сила Goku становить: {d}\n", .{entry.power});
_ = lookup.remove("Goku");
std.debug.print("Сила Goku становить: {d}\n", .{entry.power});
}
const User = struct {
power: i32,
};
У наведеному вище коді є низка тонкощів. По-перше, тепер у нас є єдиний User — goku. Значення в lookup і entry є посиланнями на goku. Наш виклик remove усе одно видаляє значення з нашого lookup, але цим значенням є просто адреса user, а не сам user. Якби ми залишилися з getPtr, ми отримали б **User, недійсний через remove. В обох рішеннях нам довелося використовувати get замість getPtr, але в цьому випадку ми просто копіюємо адресу, а не цілий User. Для великих об’єктів це може бути значною різницею.
Оскільки все це відбувається в одній функції та з невеликим значенням, як-от User, це все ще виглядає як штучно створена проблема. Нам потрібен приклад, який робить право власності на дані актуальною проблемою.
Володіння
Мені подобаються хеш-мапи, бо це структури, які всі знають і всі ними користуються. Вони також мають багато різних варіантів застосування, з більшістю з яких ви, ймовірно, стикалися на власному досвіді. Хоч їх можна використовувати як короткочасні пошукові структури, вони часто довгоживучі й тому потребують так само довгоживучих значень.
Цей код заповнює наш lookup іменами, які ви вводите в терміналі. Порожнє ім’я зупиняє цикл підказки. Нарешті, він визначає, чи було Leto серед наданих імен.
learning.zig
const std = @import("std");
const builtin = @import("builtin");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var lookup = std.StringHashMap(User).init(allocator);
defer lookup.deinit();
// stdin — це std.io.Reader,
// протилежність std.io.Writer, який ми вже бачили
const stdin = std.io.getStdIn().reader();
// stdout — це std.io.Writer
const stdout = std.io.getStdOut().writer();
var i: i32 = 0;
while (true) : (i += 1) {
var buf: [30]u8 = undefined;
try stdout.print("Будь ласка, введіть імʼя: ", .{});
if (try stdin.readUntilDelimiterOrEof(&buf, '\n')) |line| {
var name = line;
if (builtin.os.tag == .windows) {
// У Windows рядки закінчуються на \r\n.
// Нам потрібно прибрати \r
name = @constCast(std.mem.trimRight(u8, name, "\r"));
}
if (name.len == 0) {
break;
}
try lookup.put(name, .{.power = i});
}
}
const has_leto = lookup.contains("Leto");
std.debug.print("{any}\n", .{has_leto});
}
const User = struct {
power: i32,
};
Початкова версія цього коду не компілювалася у Windows. Потрібно було додати
@constCast, який ви тут бачите. Ми бачили інші вбудовані функції, але ця більш просунута. Я думав про те, щоб видалити цілий рядок, але хотів, щоб люди могли простежити за прикладом і у Windows, тому обрізання було потрібним. Для цього випадку були простіші рішення, але замість цього я вирішив залишитися з ризикованим@constCast. Я написав допис у блозі на основі цього прикладу з поясненням, навіщо він потрібен, але він суттєво складніший. До цього варто повернутися, провівши більше часу із Zig.
Код чутливий до регістру, але незалежно від того, наскільки ідеально ми вводимо «Leto», contains завжди повертає false. Виправмо це, проітерувавши lookup і вивівши ключі та значення:
// Розмістіть цей код після циклу while
var it = lookup.iterator();
while (it.next()) |kv| {
std.debug.print("{s} == {any}\n", .{kv.key_ptr.*, kv.value_ptr.*});
}
Цей шаблон ітератора поширений у Zig і ґрунтується на синергії між while і опціональними типами. Елемент ітератора повертає вказівники на наш ключ і значення, тому ми розіменовуємо їх за допомогою .*, щоб отримати доступ до самого значення, а не до адреси. Результат залежатиме від того, що ви введете, але я отримав:
Будь ласка, введіть імʼя: Paul
Будь ласка, введіть імʼя: Teg
Будь ласка, введіть імʼя: Leto
Будь ласка, введіть імʼя:
�� == learning.User{ .power = 1 }
��� == learning.User{ .power = 0 }
��� == learning.User{ .power = 2 }
false
Значення виглядають добре, але не ключі. Якщо ви не впевнені, що відбувається, можливо, це моя вина. Раніше я навмисно ввів вас в оману. Я сказав, що хеш-мапи часто довгоживучі й тому потребують довгоживучих значень. Правда полягає в тому, що вони потребують довгоживучих значень а також довгоживучих ключів! Зверніть увагу, що buf визначено всередині нашого циклу while. Коли ми викликаємо put, ми надаємо нашій хеш-мапі ключ, який має набагато коротший час життя, ніж сама хеш-мапа. Переміщення buf за межі циклу while вирішує нашу проблему з часом життя, але цей буфер повторно використовується в кожній ітерації. Це все одно не працюватиме, оскільки ми змінюємо дані ключа під ним.
Для нашого наведеного вище коду насправді є лише одне рішення: наш lookup має взяти на себе право власності на ключі. Нам потрібно додати один рядок і змінити інший:
// замініть наявний lookup.put цими двома рядками
const owned_name = try allocator.dupe(u8, name);
// name -> owned_name
try lookup.put(owned_name, .{.power = i});
dupe — це метод std.mem.Allocator, якого ми раніше не бачили. Він виділяє дублікат заданого значення. Код тепер працює, тому що наші ключі, які тепер у купі, переживають lookup. Насправді ми занадто добре подовжили час життя цих рядків: ми запровадили витоки пам’яті.
Ви могли подумати, що коли ми викликаємо lookup.deinit, наші ключі та значення звільняться за нас. Але універсального рішення, яке міг би використати StringHashMap, не існує. По-перше, ключі можуть бути рядковими літералами, які не можна звільнити. По-друге, вони могли бути створені з іншим розподільником. Нарешті, хоч і у більш просунутих сценаріях, існують законні випадки, коли ключі можуть не належати хеш-мапі.
Єдине рішення — звільнити ключі самостійно. На цьому етапі, ймовірно, мало б сенс створити наш власний тип UserLookup і інкапсулювати цю логіку очищення у функцію deinit. Ми ж залишимо все недбало як є:
// замініть існуючий
// defer lookup.deinit();
// цим:
defer {
var it = lookup.keyIterator();
while (it.next()) |key| {
allocator.free(key.*);
}
lookup.deinit();
}
Наша логіка defer — перша, яку ми бачили з блоком — звільняє кожен ключ, а потім деініціалізує lookup. Ми використовуємо keyIterator лише для ітерації ключів. Значення ітератора є вказівником на запис ключа в хеш-мапі — *[]const u8. Ми хочемо звільнити саме значення, оскільки це те, що ми виділили за допомогою dupe, тому ми розіменовуємо значення за допомогою .*.
Обіцяю, ми закінчили говорити про висячі вказівники та керування пам’яттю. Те, що ми обговорювали, може бути незрозумілим або надто абстрактним. До цього варто повернутися, коли у вас з’явиться практичніша задача. Та якщо ви плануєте написати щось нетривіальне, це те, що вам майже напевно доведеться освоїти. Коли ви почуватиметеся готовими, я закликаю вас узяти приклад циклу підказок і погратися з ним самостійно. Запровадьте тип UserLookup, який інкапсулює все керування пам’яттю, яке нам довелося зробити. Спробуйте використовувати значення *User замість User, створюючи користувачів у купі та звільняючи їх, як ми це робили з ключами. Напишіть тести, які покривають вашу нову структуру, використовуючи std.testing.allocator, щоб переконатися, що ви не втрачаєте пам’ять.
Динамічний масив ArrayList
Ви будете раді дізнатися, що можете забути про наш IntList та узагальнену альтернативу, яку ми створили. Zig має повноцінну реалізацію динамічного масиву: std.ArrayList(T).
Це досить стандартна річ, але це настільки поширена та використовувана структура даних, що варто побачити її в дії:
learning.zig
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var arr = std.ArrayList(User).init(allocator);
defer {
for (arr.items) |user| {
user.deinit(allocator);
}
arr.deinit();
}
const stdin = std.io.getStdIn().reader();
const stdout = std.io.getStdOut().writer();
var i: i32 = 0;
while (true) : (i += 1) {
var buf: [30]u8 = undefined;
try stdout.print("Будь ласка, введіть імʼя: ", .{});
if (try stdin.readUntilDelimiterOrEof(&buf, '\n')) |line| {
var name = line;
if (builtin.os.tag == .windows) {
name = @constCast(std.mem.trimRight(u8, name, "\r"));
}
if (name.len == 0) {
break;
}
const owned_name = try allocator.dupe(u8, name);
try arr.append(.{.name = owned_name, .power = i});
}
}
var has_leto = false;
for (arr.items) |user| {
if (std.mem.eql(u8, "Leto", user.name)) {
has_leto = true;
break;
}
}
std.debug.print("{any}\n", .{has_leto});
}
const User = struct {
name: []const u8,
power: i32,
fn deinit(self: User, allocator: Allocator) void {
allocator.free(self.name);
}
};
Вище — переписаний наш код із хеш-мапою, але з використанням ArrayList(User). Застосовуються ті самі правила керування часом життя та пам’яттю. Зауважте, що ми все ще робимо dupe імені і все ще звільняємо кожне ім’я перед deinit ArrayList.
Це гарний момент, щоб зазначити, що Zig не має властивостей (properties) або приватних полів. Ви можете побачити це, коли ми отримуємо доступ до arr.items, щоб ітерувати значення. Причина відсутності властивостей — усунути джерело несподіванок. У Zig, якщо це виглядає як доступ до поля, то це і є доступ до поля. Особисто я вважаю, що відсутність приватних полів — це помилка, але цю проблему, звісно, можна обійти. Я зазвичай додаю до полів префікс із підкресленням, щоб позначити «лише для внутрішнього використання».
Оскільки «типом» рядка є []u8 або []const u8, ArrayList(u8) — відповідний тип для побудовника рядків, на кшталт StringBuilder у .NET або strings.Builder у Go. Фактично, ви часто використовуватимете його, коли функція приймає Writer, а вам потрібен рядок. Раніше ми бачили приклад використання std.json.stringify для виведення JSON у stdout. Ось як можна використати ArrayList(u8) для виведення його у змінну:
learning.zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();
try std.json.stringify(.{
.this_is = "an anonymous struct",
.above = true,
.last_param = "are options",
}, .{.whitespace = .indent_2}, out.writer());
std.debug.print("{s}\n", .{out.items});
}
Anytype
У частині 1 ми коротко говорили про anytype. Це досить корисна форма качиної типізації під час компіляції. Ось простий логер:
pub const Logger = struct {
level: Level,
// "error" зарезервоване, імена всередині @"..." завжди
// трактуються як ідентифікатори
const Level = enum {
debug,
info,
@"error",
fatal,
};
fn info(logger: Logger, msg: []const u8, out: anytype) !void {
if (@intFromEnum(logger.level) <= @intFromEnum(Level.info)) {
try out.writeAll(msg);
}
}
};
Параметр out нашої функції info має тип anytype. Це означає, що наш Logger може записувати повідомлення в будь-яку структуру, яка має метод writeAll, що приймає []const u8 і повертає !void. Це не функціональність часу виконання. Перевірка типів відбувається під час компіляції, і для кожного використаного типу створюється функція з правильною типізацією. Якщо ми спробуємо викликати info з типом, який не має всіх необхідних функцій (у цьому випадку просто writeAll), ми отримаємо помилку компіляції:
var l = Logger{.level = .info};
try l.info("сервер запущено", true);
дасть нам: no field or member function named ‘writeAll’ in ‘bool’. Використання writer від ArrayList(u8) працює:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var l = Logger{.level = .info};
var arr = std.ArrayList(u8).init(allocator);
defer arr.deinit();
try l.info("сервер запущено", arr.writer());
std.debug.print("{s}\n", .{arr.items});
}
Одним зі значних недоліків anytype є документація. Ось сигнатура функції std.json.stringify, яку ми використовували кілька разів:
// Я **ненавиджу** багаторядкові визначення функцій
// Але я зроблю виняток для посібника, який
// можливо, ви читаєте на маленькому екрані.
fn stringify(
value: anytype,
options: StringifyOptions,
out_stream: anytype
) @TypeOf(out_stream).Error!void
Перший параметр, value: anytype, начебто очевидний. Це значення для серіалізації, і воно може бути будь-яким (насправді є речі, які JSON-серіалізатор Zig серіалізувати не може). Ми можемо здогадатися, що out_stream — це те, куди записувати JSON, але щодо того, які методи має бути реалізовано, ваші припущення не гірші за мої. Єдиний спосіб з’ясувати це — прочитати вихідний код або, як альтернатива, передати фіктивне значення та використати помилки компілятора як документацію. Це те, що можна було б покращити кращими автоматичними генераторами документації. Але вже не вперше я шкодую, що Zig не має інтерфейсів.
@TypeOf
У попередніх частинах ми використовували @TypeOf, щоб допомогти нам перевірити тип різних змінних. З нашого використання вам можна пробачити, якщо ви подумали, що вона повертає назву типу як рядок. Однак, враховуючи, що це функція в PascalCase, ви маєте знати краще: вона повертає type.
Одне з моїх улюблених застосувань anytype — це поєднання його зі вбудованими функціями @TypeOf і @hasField для написання тестових помічників. Хоч кожен тип користувача, який ми бачили, був дуже простим, я попрошу вас уявити складнішу структуру з багатьма полями. У багатьох наших тестах нам потрібен User, але ми хочемо задавати лише поля, що стосуються тесту. Створімо userFactory:
fn userFactory(data: anytype) User {
const T = @TypeOf(data);
return .{
.id = if (@hasField(T, "id")) data.id else 0,
.power = if (@hasField(T, "power")) data.power else 0,
.active = if (@hasField(T, "active")) data.active else true,
.name = if (@hasField(T, "name")) data.name else "",
};
}
pub const User = struct {
id: u64,
power: u64,
active: bool,
name: [] const u8,
};
Користувача за замовчуванням можна створити, викликавши userFactory(.{}), або ми можемо перевизначити певні поля за допомогою userFactory(.{.id = 100, .active = false}). Це невеликий патерн, але мені він дуже подобається. Це також гарний перший крок у світ метапрограмування.
Частіше за все @TypeOf поєднується з @typeInfo, який повертає std.builtin.Type. Це потужне об’єднання з тегами, що повністю описує тип. Функція std.json.stringify рекурсивно використовує це на наданому value, щоб з’ясувати, як його серіалізувати.
Система збирання Zig
Якщо ви прочитали весь цей посібник, чекаючи розуміння того, як створювати складніші проєкти з кількома залежностями та різними цілями, то невдовзі будете розчаровані. Zig має настільки потужну систему збирання, що все більше проєктів, які не пов’язані з Zig, використовують її — наприклад, libsodium. На жаль, уся ця потужність означає, що для простіших потреб система не дуже проста у використанні чи розумінні.
Правда в тому, що я недостатньо добре розумію систему збирання Zig, щоб її пояснити.
Однак ми можемо отримати принаймні короткий огляд. Щоб запустити наш Zig-код, ми використовували zig run learning.zig. Одного разу ми також використали zig test learning.zig для запуску тесту. Команди run і test чудові для експериментів, але саме команда build знадобиться для чогось складнішого. Команда build спирається на файл build.zig зі спеціальною точкою входу build. Ось скелет:
learning.zig
const std = @import("std");
pub fn build(b: *std.Build) !void {
_ = b;
}
Кожна збірка має типовий крок «install», який тепер можна виконати за допомогою zig build install, але оскільки наш файл здебільшого порожній, ви не отримаєте жодних значущих артефактів. Нам потрібно повідомити нашій збірці про точку входу нашої програми, яка знаходиться в learning.zig:
learning.zig
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// налаштування виконуваного файлу
const exe = b.addExecutable(.{
.name = "learning",
.target = target,
.optimize = optimize,
.root_source_file = b.path("learning.zig"),
});
b.installArtifact(exe);
}
Тепер, якщо ви запустите zig build install, ви отримаєте двійковий файл у ./zig-out/bin/learning. Використання стандартних цілей і опцій оптимізації дає нам змогу перевизначати значення за замовчуванням через аргументи командного рядка. Наприклад, щоб зібрати оптимізовану за розміром версію нашої програми для Windows, ми б зробили:
zig build install -Doptimize=ReleaseSmall -Dtarget=x86_64-windows-gnu
Виконуваний файл часто має два додаткові кроки окрім стандартного install: run та test. Бібліотека може мати один крок — test. Для базового run без аргументів нам потрібно додати чотири рядки в кінець нашої збірки:
// додайте після: b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Start learning!");
run_step.dependOn(&run_cmd.step);
Це створює дві залежності за допомогою двох викликів dependOn. Перший прив’язує нашу нову команду run до вбудованого кроку install. Другий пов’язує крок run з новоствореною командою run. Вам може бути цікаво, навіщо потрібна і команда run, і крок run. Я гадаю, що це розділення існує для підтримки складніших налаштувань: кроків, які залежать від кількох команд, або команд, які використовуються в кількох кроках. Якщо ви запустите zig build --help і прокрутите догори, ви побачите наш новий крок «run». Тепер ви можете запустити програму, виконавши zig build run.
Щоб додати крок «test», ви скопіюєте більшу частину коду run, який ми щойно додали, але замість b.addExecutable почнете з b.addTest:
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("learning.zig"),
});
const test_cmd = b.addRunArtifact(tests);
test_cmd.step.dependOn(b.getInstallStep());
const test_step = b.step("test", "Run the tests");
test_step.dependOn(&test_cmd.step);
Ми дали цьому кроку назву «test». Запуск zig build --help має показати ще один доступний крок — «test». Оскільки у нас немає тестів, важко сказати, працює воно чи ні. У learning.zig додайте:
test "dummy build test" {
try std.testing.expectEqual(false, true);
}
Тепер, коли ви запускаєте zig build test, ви маєте отримати збій тесту. Якщо ви виправите тест і знову запустите zig build test, ви не отримаєте жодного виводу. За замовчуванням тестовий runner Zig виводить щось лише в разі помилки. Використовуйте zig build test --summary all, якщо, як і я, ви завжди хочете підсумок незалежно від того, пройдено тест чи ні.
Це мінімальна конфігурація, потрібна вам для старту. Але будьте спокійні: якщо вам це потрібно зібрати, Zig, ймовірно, впорається. Нарешті, ви можете і, мабуть, маєте використовувати zig init у корені вашого проєкту, щоб Zig створив для вас добре задокументований файл build.zig.
Сторонні залежності
Вбудований менеджер пакетів Zig відносно новий і через це має низку недоліків. Хоч є простір для вдосконалення, його можна використовувати як є. Нам потрібно розглянути дві частини: створення пакета та використання пакетів. Пройдемо обидві.
Спочатку створіть нову теку під назвою calc і створіть три файли. Перший — це add.zig із таким вмістом:
// Ой, прихований урок: подивіться на тип b
// і тип повернення!!
pub fn add(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
return a + b;
}
const testing = @import("std").testing;
test "add" {
try testing.expectEqual(@as(i32, 32), add(30, 2));
}
Це трохи безглуздо — цілий пакет, щоб лише додати два значення, — але це дасть нам змогу зосередитися на аспекті пакування. Далі ми додамо не менш безглуздий calc.zig:
pub const add = @import("add.zig").add;
test {
// За замовчуванням включаються лише тести у вказаному
// файлі. Цей чарівний рядок коду викличе
// посилання на всі вкладені контейнери,
// щоб їх перевірити.
@import("std").testing.refAllDecls(@This());
}
Ми розділяємо це між calc.zig і add.zig, щоб показати, що zig build автоматично збере та запакує всі файли нашого проєкту. Нарешті, ми можемо додати build.zig:
learning.zig
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("calc.zig"),
});
const test_cmd = b.addRunArtifact(tests);
test_cmd.step.dependOn(b.getInstallStep());
const test_step = b.step("test", "Run the tests");
test_step.dependOn(&test_cmd.step);
}
Це все повторення того, що ми бачили в попередньому розділі. З цим ви можете запустити zig build test --summary all.
Повернемося до нашого навчального проєкту і раніше створеного build.zig. Ми почнемо з додавання нашого локального calc як залежності. Нам потрібно зробити три доповнення. Спочатку створимо модуль, який вказує на наш calc.zig:
// Ви можете розмістити це у верхній частині функції build,
// перед викликом addExecutable.
const calc_module = b.addModule("calc", .{
.root_source_file = b.path("PATH_TO_CALC_PROJECT/calc.zig"),
});
Вам потрібно буде змінити шлях до calc.zig. Тепер нам потрібно додати цей модуль до наших наявних змінних exe і tests. Оскільки наш build.zig стає насиченішим, ми спробуємо трохи впорядкувати:
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const calc_module = b.addModule("calc", .{
.root_source_file = b.path("PATH_TO_CALC_PROJECT/calc.zig"),
});
{
// налаштовуємо "run" команду
const exe = b.addExecutable(.{
.name = "learning",
.target = target,
.optimize = optimize,
.root_source_file = b.path("learning.zig"),
});
// додайте це
exe.root_module.addImport("calc", calc_module);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Start learning!");
run_step.dependOn(&run_cmd.step);
}
{
// налаштовуємо "test" команду
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("learning.zig"),
});
// додайте це
tests.root_module.addImport("calc", calc_module);
const test_cmd = b.addRunArtifact(tests);
test_cmd.step.dependOn(b.getInstallStep());
const test_step = b.step("test", "Run the tests");
test_step.dependOn(&test_cmd.step);
}
}
Зі свого проєкту тепер ви можете @import("calc"):
const calc = @import("calc");
...
calc.add(1, 2);
Додавання віддаленої залежності вимагає трохи більше зусиль. По-перше, нам потрібно повернутися до проєкту calc і визначити модуль. Ви можете подумати, що проєкт сам по собі і є модулем, але проєкт може виставляти кілька модулів, тому нам потрібно створити його явно. Ми використовуємо той самий addModule, але відкидаємо повернуте значення. Простого виклику addModule достатньо, щоб визначити модуль, який потім зможуть імпортувати інші проєкти.
_ = b.addModule("calc", .{
.root_source_file = b.path("calc.zig"),
});
Це єдина зміна, яку нам потрібно внести в нашу бібліотеку. Оскільки це вправа з віддалених залежностей, я виклав цей проєкт calc на GitHub, щоб ми могли імпортувати його в наш навчальний проєкт. Він доступний за адресою https://github.com/karlseguin/calc.zig.
У нашому навчальному проєкті нам потрібен новий файл — build.zig.zon. «ZON» розшифровується як Zig Object Notation і дозволяє виражати дані Zig у зрозумілому для людини форматі та перетворювати цей формат на Zig-код. Вміст build.zig.zon буде таким:
.{
.name = "learning",
.paths = .{""},
.version = "0.0.0",
.dependencies = .{
.calc = .{
.url = "https://github.com/karlseguin/calc.zig/archive/d1881b689817264a5644b4d6928c73df8cf2b193.tar.gz",
.hash = "12ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
},
},
}
У цьому файлі є два сумнівних значення. Перше — d1881b689817264a5644b4d6928c73df8cf2b193 в url. Це просто хеш git-коміту. Друге — значення hash. Наскільки я знаю, наразі немає гарного способу з’ясувати, яким має бути це значення, тому поки що ми використовуємо фіктивне.
Щоб використовувати цю залежність, нам потрібно внести одну зміну до нашого build.zig:
// замініть це:
const calc_module = b.addModule("calc", .{
.root_source_file = b.path("calc/calc.zig"),
});
// цим:
const calc_dep = b.dependency("calc", .{.target = target, .optimize = optimize});
const calc_module = calc_dep.module("calc");
У build.zig.zon ми назвали залежність calc, і це та залежність, яку ми тут завантажуємо. З цієї залежності ми беремо модуль calc, оскільки в build.zig ми назвали модуль calc.
Якщо ви спробуєте запустити zig build test, ви маєте побачити помилку:
hash mismatch: manifest declares
122053da05e0c9348d91218ef015c8307749ef39f8e90c208a186e5f444e818672da
but the fetched package has
122036b1948caa15c2c9054286b3057877f7b152a5102c9262511bf89554dc836ee5
Скопіюйте та вставте правильний хеш назад у build.zig.zon і спробуйте знову запустити zig build test. Тепер усе має працювати.
Це звучить як багато, і я сподіваюся, що з часом усе спроститься. Але здебільшого це те, що можна скопіювати та вставити з інших проєктів, і після налаштування можна рухатися далі.
Коротке попередження: я виявив, що Zig агресивно кешує залежності. Якщо ви намагаєтеся оновити залежність, але Zig, здається, не помічає змін… що ж, я видаляю теку zig-cache проєкту, а також ~/.cache/zig.
Ми охопили багато інформації, дослідивши кілька основних структур даних і об’єднавши великі фрагменти з попередніх частин. Наш код став трохи складнішим, з меншим фокусом на конкретному синтаксисі і більшою схожістю на справжній код. Я в захваті від того, що, попри цю складність, код переважно мав сенс. Якщо ні — не здавайтеся. Виберіть приклад і розберіть його на частини, додайте оператори виведення, напишіть кілька тестів. Працюйте з кодом, створюйте свій власний, а потім поверніться і перечитайте ті частини, які не мали сенсу.