We rewrote the Ghostty GTK application 🔥 Горячее 💬 Длинная дискуссия
Ghostty GTK-часть переписана с нуля
Проект завершён: Linux/BSD-версия теперь полностью использует GObject из Zig и проверена Valgrind.
Что изменилось
- Zig-структуры обёрнуты в GObject; память управляется счётчиками ссылок.
- При обновлении конфига старый объект освобождается автоматически, когда исчезают ссылки.
- Появились сигналы, свойства, действия GTK и современные UI-файлы Blueprint.
- Новые виджеты добавляются быстрее (анимированная рамка, вкладки в заголовке и т.д.).
Valgrind Каждый коммит прогонялся под Valgrind.
- Потребовался большой suppression-файл (в основном GTK/драйверы).
- Найдены: утечка и неопределённое обращение в Zig-коде, ошибка
WeakRef
в GTK, которые иначе вызывали редкие краши.
Комментарии (194)
- Ghostty переписывает GUI на GTK/Zig, столкнувшись с проблемами GObject и управления памятью.
- Участники обсуждают, что GTK навязывает свою ООП-модель и счётчики ссылок, что сложно сочетать с Zig/Rust.
- Некоторые считают GTK не «родной» на Linux и предлагают Vala, Qt или собственный UI.
- Пользователи жалуются на баги прокрутки, копирования, nano и отсутствие поиска.
- Автор признаёт выбор GTK компромиссом ради кроссплатформенности и «родного» вида.
Faster substring search with SIMD in Zig
SIMD-поиск подстроки в Zig
Автор реализовал алгоритм, который на 60 % быстрее std.mem.indexOf
.
Идея: сравниваем 32-байтовые блоки текста с первым и последним символом искомого слова, используя AVX2.
- Берём первый и последний байт
needle
(first
,last
). - Загружаем 32 байта
haystack
в векторBlock = @Vector(32, u8)
. - Создаём маски совпадений:
const eq_first = first == block; const eq_last = last == block_shifted; const mask = @bitCast(eq_first & eq_last);
- Для каждого установленного бита проверяем полное совпадение подстроки.
- Хвост обрабатываем обычным
indexOf
.
Код (сокращённо):
const Block = @Vector(32, u8);
const first = @splat(needle[0]);
const last = @splat(needle[k-1]);
while (i + k + 32 <= n) : (i += 32) {
const f = haystack[i..][0..32].*;
const l = haystack[i+k-1..][0..32].*;
var mask: u32 = @bitCast((first == f) & (last == l));
while (mask != 0) {
const bit = @ctz(mask);
if (mem.eql(u8, haystack[i+bit+1..][0..k-1], needle[1..]))
return i + bit;
mask &= mask - 1;
}
}
return mem.indexOf(u8, haystack[i..], needle) orelse null;
Тест: поиск слова «newsletter» во всём «Моби Дике» (~1.2 МБ).
Сборка: zig build -Doptimize=ReleaseFast
.
Комментарии (52)
- Подход с SIMD-ускорением поиска подстроки популярен, но в худшем случае остаётся квадратичным
O(m·n)
, поэтому нужен «откат» на линейный алгоритм (KMP/BM). - Участники отмечают, что большинство реализаций опираются на 10-летние AVX/NEON, игнорируя AVX-512, SVE и RVV, которые дают больший выигрыш, но пока редки на десктопах и в облаках.
- Zig пока не предоставляет прямых intrinsics, хотя LLVM-бекенд позволяет вызывать нужные инструкции; это тормозит портирование низкоуровневых оптимизаций.
- Есть идея дальнейшего SIMD-фильтра: проверять не только первый/последний байт иглы, но и второй/предпоследний и т.д., накладывая маски.
- Вопросы Unicode: алгоритм работает на байтах, поэтому для UTF-8/16 потребуется дополнительная обработка переменной длины кодов.
Zig's Lovely Syntax 💬 Длинная дискуссия
Zig выглядит почти как Rust, но делает синтаксис ещё приятнее за счёт более простой семантики и ряда изящных решений.
Числа
Литералы 92
всегда имеют тип comptime_int
; при присваивании они неявно приводятся к нужному типу. Суффиксов нет.
Строки
Многострочные «сырые» строки пишутся через \\
в начале каждой строки; \
не экранируется, отступы не портятся, а лексер работает построчно.
Структуры
.{ .x = 1, .y = 2 }
— запись поля через .x =
совпадает с присваиванием, что позволяет грепом находить именно записи, а не чтения.
Типы
Все типы префиксные: u32
, [3]u32
, ?[3]u32
, *const ?[3]u32
. Разыменование постфиксное: ptr.*
.
Идентификаторы
Синтаксис @"имя с пробелом"
позволяет обходить ключевые слова и экспортировать любые имена.
Функции
fn add(x: i32, y: i32) i32
— без стрелки ->
, так как лямбд нет, а возвращаемый тип всегда обязателен. pub fn main() void {}
.
Переменные
const
и var
; часто используемое const
короче, чем в Rust, но всё же длиннее Kotlin-овского val
.
Комментарии (156)
- Обсуждение разделилось: кому-то синтаксис Zig кажется «прекрасным» минимализмом, другим — «шумным» и «капризным».
- Спор о порядке «имя: тип» vs «тип имя»: одни хотят видеть тип первым, другие — имя.
- Критика деталей: @-префиксы,
.{}
, отсутствие лямбд, перенос строк,orelse
без пробела. - Плюсы: raw-строки Zig решают проблему отступов; обработка ошибок через
try
нравится многим. - Сравнения: Kotlin, C#, Go, Rust, D — каждый считает «своё» лучше.
- Итог: «красота» синтаксиса субъективна и во многом привычна; после практики Zig начинает нравиться.
Debian GNU/Hurd 2025 released
Debian GNU/Hurd 2025 — неофициальный релиз Debian, но официальный порт.
Основан на «sid» в момент выхода «Trixie».
- ISO: hurd-i386 | hurd-amd64
- Образы и инструкции: hurd-install
Архитектуры: i386, amd64; покрытие ~72 % архива Debian.
Новое
- 64-бит полностью готов, драйверы дисков через Rump (NetBSD).
- xattr по умолчанию для трансляторов; mmdebstrap из других ОС.
- Порт Rust, USB-диски и CD через Rump.
- SMP, xkb-раскладки, framebuffer, acpi, rtc, apic, hpet.
- Исправления irqs, nfsv3, libports, pipes и др.
Документация
Присоединяйтесь: contributing
Комментарии (107)
- Hurd — давний проект с микроядром Mach, фактически движимый одним человеком (Samuel Thibault), и сегодня выглядит скорее хобби-экспериментом, чем реальной альтернативой Linux.
- Участники обсуждают низкую поддержку железа, архаичную архитектуру и отсутствие прогресса: «ждать совершенства — значит никогда не стать готовым».
- Предлагаются новые микроядра (seL4, L4, Viengoos) и языки (Rust, Zig), но критика считает это погоней за хайпом.
- GUIX/Nix и GNU Shepherd упоминаются как более живые GNU-ориентированные проекты; GUIX уже умеет запускать Hurd в виртуалке.
- Итог: Hurd остаётся интересным музейным экспонатом и источником идей, но не готов к «повседневному» использованию.
Комментарии (89)
- Zig вызывает интерес благодаря мощному comptime и «inline else», позволяющим абстрагироваться без рантайм-оверхеда.
- Участники сравнивают его метапрограммирование с C, D и Rust, отмечая, что похожие идеи уже были, но Zig может сделать их популярнее.
- Главный упрек Zig — отсутствие гарантий memory- и data-race safety, из-за чего многие считают его неподходящим для многопоточного кода.
- «comptime unreachable» воспринимается как способ доказать компилятору недостижимость кода, а не как runtime-assert.
- Некоторые считают, что язык ещё нестабилен и экосистема незрела, поэтому широкое внедрение отложено.
A fast, growable array with stable pointers in C
Моя предыдущая статья о обобщённых структурах данных в C готовила почву к теме: структура, которая заменяет динамические массивы, даёт стабильные указатели и хорошо работает с аренными аллокаторами. Её переоткрывали много раз под разными именами: “levelwise-allocated pile” (2001), в Zig — Segmented List, частично похожая на C++ std::deque. Мне нравится название Per Vognsen — Segment Array.
Скачать мой однофайловый заголовок segment_array.h можно, подписавшись на рассылку.
Идея проста: фиксированный массив указателей на сегменты; каждый следующий сегмент вдвое больше предыдущего; новые сегменты выделяются по мере необходимости. Поскольку элементы не двигаются, указатели на них стабильны, не остаются “дыры” в арене, а доступ по индексу — за O(1).
Реализация
Структура на C:
typedef struct { u32 count; int used_segments; u8 *segments[26]; } SegmentArrayInternal;
Почему всего 26 сегментов? Из 64 бит указателя обычно реально используются 48, так что 49 сегментов уже перекрывают адресное пространство (~256 ТиБ). Я предпочитаю индекс u32 (до ~4 млрд элементов) — это даёт 32 сегмента. Ещё убираем 6 маленьких (1..32), начинаем с 64, остаётся 26 сегментов — хватает для 4 294 967 232 элементов (чуть меньше UINT32_MAX). Фиксированный массив рядом со структурой снижает риск промаха кэша.
Размеры сегментов — степени двойки: проще математика и быстрые сдвиги для индексов.
#define SMALL_SEGMENTS_TO_SKIP 6
#define log2i(X) ((u32) (8*sizeof(unsigned long long)
- __builtin_clzll((X)) - 1))
u32 capacity_for_segment_count(int segment_count) { return ((1 << SMALL_SEGMENTS_TO_SKIP) << segment_count) - (1 << SMALL_SEGMENTS_TO_SKIP); }
void *_sa_get(SegmentArrayInternal sa, u32 index, size_t item_size) { int segment = log2i((index >> SMALL_SEGMENTS_TO_SKIP) + 1); u32 slot = index - capacity_for_segment_count(segment); return sa->segments[segment] + item_sizeslot; }
log2i использует __builtin_clzll (подсчёт ведущих нулей) для быстрого вычисления номера сегмента.
Clang оптимизирует _sa_get до ~10 инструкций x86-64 (-O3), так что узким местом будет память, а не вычисления индекса. При последовательной итерации можно обходить сегменты напрямую; в segment_array.h есть макрос.
Выделение нового элемента:
u32 slots_in_segment(int segment_index) { return (1 << SMALL_SEGMENTS_TO_SKIP) << segment_index; }
void *_sa_alloc(SegmentArrayInternal *sa, size_t item_size) { if (sa->count >= capacity_for_segment_count(sa->used_segments)) { size_t segment_size = item_size * slots_in_segment(sa->used_segments); sa->segments[sa->used_segments++] = malloc(segment_size); } sa->count++; return _sa_get(sa, sa->count-1, item_size); }
Замечание: можно сделать ёмкость строго степенью двойки, если первые два сегмента одинакового размера. Код станет менее изящным, но это спасает от ~50% потерь памяти при использовании как массива бакетов в хеш-таблице со степенью двойки.
Дженерики
Я применяю технику из прошлой статьи для типобезопасного хранения любого типа. Макрос связывает тип с общей структурой:
#define SegmentArray(type)
union {
SegmentArrayInternal internal;
type *payload;
}
Дальше макросы используют payload, чтобы передавать сведения о типе…
Комментарии (77)
- Обсуждается структура «сегментированный массив» (экспоненциальные сегменты), её плюсы и минусы, и сравнение с существующими решениями: std::deque, ropes, Zig std.SegmentedList, rust-array-stump, plf::colony.
- Критика терминологии: это не «массив» в классическом смысле из‑за неконтигуозной памяти; многие API ожидают сплошной/страйдовый буфер и не подойдут.
- Производительность: при локальных L1-итерациях вычислительная часть индексации может быть ощутима; для больших объёмов память становится бутылочным горлышком. Предлагаются оптимизации итерации по сегментам и замечания про clz/bsr/lzcnt и опции компилятора.
- Виртуальная память как альтернатива: резервирование большого диапазона и по мере роста коммит страниц/guard pages; отмечены плюсы на Linux (MAP_POPULATE, mremap), но плохо для embedded/WASM.
- Сравнение с deque: фиксированные блоки vs экспоненциальные, поддержка prepend, рандом-доступ есть; реализация MSVC критикуется за малый размер блока, GNU/libc++ лучше.
- Недостатки сегментов: ухудшение предвыборки/кэш-локальности при линейной итерации, отсутствие стабильной непрерывности для API, сложность с хеш-таблицами при росте (rehash), потенциальный перерасход памяти при экспоненциальных размерах.
- Предложения: настраиваемый минимальный размер сегмента, функции «склейки» мелких сегментов, разбор условий, когда экспоненциальные сегменты оправданы, и замечания о чрезмерной макротрюковости в C/C23.
Zig Error Patterns
Введение
Я часто использую отладчик, но привык и к выводной отладке, особенно в юнит-тестах. Хотелось улучшить её и чаще подключать отладчик.
Улучшение выводной отладки
Главная проблема — «шум»: в цикле интересна одна итерация, а печатается всё. Или удобнее читать форматированную структуру, но приходится раскидывать print’ы по коду. В Zig тесты используют error’ы, значит можно печатать только при падении теста через errdefer:
test { errdefer std.debug.print("{f}", .{ast}); // ... }
Так контекст появляется только при ошибке, без засорения лога.
Запуск тестов в отладчике
Просто запустить seergdb или gdb -tui неудобно: тестовые бинарники лежат в zig-cache. Трюк из ziggит: build.zig может запускать команды и передавать путь артефакта:
// seergdb — GUI фронтенд для gdb const debugger = b.addSystemCommand(&.{ "seergdb", "--run", "--" }); debugger.addArtifactArg(exe_unit_tests);
const debug_step = b.step("debug", "Run unit tests under debugger"); debug_step.dependOn(&debugger.step);
Это запускает правильный бинарник. Но отладчик сработает лишь на брейкпоинте или панике, тогда как раннер тестов «проглатывает» ошибки.
Комбинация трюков
Добавим @breakpoint через errdefer:
test { errdefer @breakpoint(); }
Так мы попадаем в точку ошибки, видим контекст и вывод std.testing.expect*. Минус: при zig build test отчёт показывает падение всего шага тестов, а не отдельных кейсов. Нужна возможность включать брейкпоинты выборочно.
Условная компиляция
Через build options пробрасываем флаг, решающий, вызывать ли @breakpoint в тестах.
Минимальный скрипт сборки, запускающий тесты, дополняем опциями:
const std = @import("std");
pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{});
const lib = b.addModule("zig-test-patterns", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const options = b.addOptions();
options.addOption(bool, "debugger", false);
lib.addImport("config", options.createModule());
const mod_tests = b.addTest(.{ .root_module = lib });
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}
В коде тестов:
const std = @import("std"); const config = @import("config");
test "errdefer @breakpoint()" { errdefer if (config.debugger) @breakpoint(); return error.FixMe; }
test "no breakpoint" { return error.FixMe; }
zig build test — без брейкпоинтов. Но менять значение флага так — значит пересобирать build.zig. Добавим опцию прямо в систему сборки:
var options = b.addOptions(); const use_debugger = b.option( bool, "debugger", "Enables code intended to only run under a debugger", ) orelse false; options.addOption(bool, "debugger", use_debugger);
Теперь можно переключать поведением командой:
zig build -Ddebugger test
И, при желании, привязать шаг запуска отладчика к этому флагу.
Комментарии (46)
- Участники хвалят согласованность базовых конструкций Zig: минимализм синтаксиса и мощь comptime позволяют элегантные решения без излишней сложности.
- Особый интерес вызвал errdefer: многие отмечают, что это упрощает тесты и отладку; звучит мнение, что такую возможность «стоит иметь каждому языку».
- Обсуждают практики отладки: полезны советы по интеграции дебаггера в build.zig, что избавляет от ручного поиска исполняемого файла в кэше.
- Поднимается вопрос об ошибках без полезной нагрузки в Zig: при парсинге (например, JSON) типовые ошибки вроде UnexpectedToken недостаточно информативны; интересуются паттернами передачи дополнительного контекста.
- Есть замечание о смешении стилей именования (camelCase в stdlib vs snake_case у автора), что может сбивать с толку.
- Отмечают эстетику сайта и блога: шрифты (Berkeley Mono), цветовую схему и ретро-оформление — «как в старых DOS-играх».
- Проводится параллель с D: аналогичная идея реализована через scope(failure), что подчеркивает общность концепции.
Comptime.ts: compile-time expressions for TypeScript
Простой компилятор TypeScript для вычисления выражений с пометкой comptime на этапе сборки. Полезно для переноса вычислений из рантайма в компиляцию. Вдохновлено Bun macros и Zig comptime.
Внимание: вы сами отвечаете за безопасность выражений, вычисляемых на этапе компиляции. Изоляции нет. Импорты comptime допускаются только в файлах проекта (не в node_modules), но можно импортировать из node_modules как comptime.
Содержание
- Что такое comptime.ts?
- Примеры: 1) простая сумма; 2) CSS без рантайма; 3) константы во время сборки
- Установка
- Использование: Vite, Bun, CLI, API
- Принудительная оценка и промисы, отказ от «вирусности»
- Запуск кода после comptime, как работает, ограничения, практики, отладка, поддержка, лицензия
Что это comptime.ts вычисляет выражения при компиляции, сокращая работу в рантайме.
Примеры
-
Простая сумма import { sum } from "./sum.ts" with { type: "comptime" }; console.log(sum(1, 2)); // => console.log(3);
-
Emotion CSS без рантайма import { css } from "@emotion/css" with { type: "comptime" }; const style = css
color: red; font-size: 16px;
; div({ class: style }); // => const style = "css-x2wxma"; div({ class: style });
Примечание: импорт @emotion/css удаляется. Стили нужно вывести отдельно (после comptime или плагином бандлера).
- Константы на этапе сборки import { ms } from "ms" with { type: "comptime" }; const HOUR = ms("1 hour"); // => const HOUR = 3600000;
Поддерживаются многие выражения (включая индексацию и импортированные константы), результат должен быть сериализуем в JSON. Импорты с type: "comptime" удаляются; лишнее убирает ваш бандлер.
Установка bun add comptime.ts pnpm add comptime.ts npm install comptime.ts
Использование
- Vite: import { comptime } from "comptime.ts/vite"; export default defineConfig({ plugins: [comptime()] });
Только в прод-сборке, если поведение совпадает с рантаймом: export default defineConfig({ build: { rollupOptions: { plugins: [comptime()] } } });
- Bun: import { comptime } from "comptime.ts/bun"; await Bun.build({ entrypoints: ["./index.ts"], ou ... })
Комментарии (30)
- Обсуждение крутится вокруг идеи “comptime”/макросов в JS: часть хочет Rust‑подобные макросы и proc‑макросы (вплоть до JSX как jsx! или вообще писать фронт на Rust/wasm), другая сторона категорически против макросов в TS/JS.
- Есть путаница в терминах: “макросы” vs “comptime”; участники критикуют переиспользование терминов и вспоминают неудачный опыт sweet.js.
- Практические вопросы: можно ли делать агрессивный dead‑code elimination через if (comptime …) как в C препроцессоре? Ответ: само comptime подставит true/false, но для выкидывания веток нужен отдельный шаг минификатора/бандлера (Vite/Bun поддержат).
- Дискуссия об импорте with { type: 'comptime' }: одни считают это неправильным использованием атрибута type (ожидается соответствие MIME), другие указывают, что спецификация оставляет семантику type открытой.
- Обсуждают границы возможности: поддержка типов/генериков на уровне comptime (как в Zig) пока ограничена; возврат именованных функций и сложные случаи с замыканиями не поддерживаются из‑за требований к гарантиям и сохранению функций между процессами.
- Альтернативы: настроить сборку для JSX без макросов; использовать библиотеки вроде lite-jsx; для Rust‑фронта рекомендуют Dioxus/Leptos; спорят о реальной применимости wasm и памяти/управления ей в вебе.
- Применимость: идея удобна для предсборки (например, markdown) и константной подстановки, но не заменяет полноценных препроцессоров/макросистем уровня Rust/Zig.