Последние несколько лет я, похоже, одержим двумя темами: а) Nix в качестве инструмента для исследования новаторских идей, требующих возможностей перестройки мира; б) замена ELF на SQLite в качестве формата исполняемых файлов. Возможно, вы заметили, что эти две идеи хорошо сочетаются друг с другом.Я изучал вторую идею в рамках своей диссертации, однако реакции окружающих оказались довольно разочаровывающими. Радикальные идеи сложно продвигать, ведь приходится бороться с инерцией давно устоявшегося решения.Одним из результатов этого исследования стал инструмент sqlelf, позволяющий декларативно исследовать файл ELF при помощи SQL. [Я написал статью arXiv:2405.03883, которую мне не удалось опубликовать]. SELECT name FROM elf_symbols вместо возни с readelf и grep. Это оказалось на удивление просто благодаря использованию виртуальных таблиц для ELF, однако мне всё равно было интересно исследовать формат файлов ELF. Но всё же я понимал, что можно сделать нечто гораздо большее.Меня всё не покидала эта мысль, а в свете прогресса LLM мне показалась привлекательной идея исследовать эту тему глубже. В частности, мне было любопытно, что будет, если мы заменим ELF на SQLite в качестве формата исполняемых файлов?Не просто «база данных, описывающая исполняемый файл», а реальный файл, с которым можно сделать chmod +x и запустить его. Читать далее

Последние несколько лет я, похоже, одержим двумя темами: а) Nix в качестве инструмента для исследования новаторских идей, требующих возможностей перестройки мира; б) замена ELF на SQLite в качестве формата исполняемых файлов. Возможно, вы заметили, что эти две идеи хорошо сочетаются друг с другом.
Я изучал вторую идею в рамках своей диссертации, однако реакции окружающих оказались довольно разочаровывающими. Радикальные идеи сложно продвигать, ведь приходится бороться с инерцией давно устоявшегося решения.
Одним из результатов этого исследования стал инструмент sqlelf, позволяющий декларативно исследовать файл ELF при помощи SQL. [Я написал статью arXiv:2405.03883, которую мне не удалось опубликовать]. SELECT name FROM elf_symbols вместо возни с readelf и grep. Это оказалось на удивление просто благодаря использованию виртуальных таблиц для ELF, однако мне всё равно было интересно исследовать формат файлов ELF. Но всё же я понимал, что можно сделать нечто гораздо большее.
Меня всё не покидала эта мысль, а в свете прогресса LLM мне показалась привлекательной идея исследовать эту тему глубже. В частности, мне было любопытно, что будет, если мы заменим ELF на SQLite в качестве формата исполняемых файлов?
Не просто «база данных, описывающая исполняемый файл», а реальный файл, с которым можно сделать chmod +x и запустить его.
$ file hello
hello: SQLite 3.x database, application id 0x53454c46, user version 1
$ ./hello
Hello, world!
$ sqlite3 hello 'SELECT soname FROM ldd'
libc.so.6Я разработал достаточно функциональный прототип, который назвал SELF — Structured Executable & Linkable Format. Если вам любопытно, его можно изучить на GitHub. Меня приятно удивили любопытные последствия этой идеи.
ELF — база данных, которая отказывается это признаватьКогда я работал над диссертацией, ко мне пришла мысль, не дававшая мне покоя: ELF — это уже база данных. Он просто вручную реализует многие примитивы баз данных, а также на удивление большое количество структур данных ради производительности, например, фильтр Блума для поиска символов.
Механизм ELF | Примитив баз данных, который он переизобрёл |
|---|---|
| интернирование строк |
| индекс ( |
таблица заголовков разделов |
|
| реализуемый вручную внешний ключ |
| структура записи страницы B-дерева |
| столбец |
|
|
кэш | индексы out-of-band для указанного выше |
Если вам когда-нибудь приходилось анализировать или парсить ELF, ядро, ld.so, binutils, LIEF, goblin, readelf, то вы снова и снова реализуете один и тот же парсер. Каждый производитель заново реализует один и тот же сериализатор.
Сам формат невероятно сжатый, он разрабатывался для мира, в котором дисковое пространство и ширина сетевого канала находятся в большом дефиците. Модифицировать этот формат сложно, из-за плотной упаковки часто приходится исключать разделы и добавлять новые. У формата нет самоописываемой схемы. ELF — это очень обобщённый формат, поддерживающий разделы данных, по стандарту интерпретируемых конкретным образом, но сам формат к этому не принуждает.
SQLite можно назвать контрпримером. Это самоописываемый и крайне стабильный формат. Он спроектирован так, чтобы расширяться для поддержки новых фич без поломки уже имеющихся потребителей, и поддерживает широкий спектр запросов, обеспечивая при этом высокую производительность.
Если бы мы захотели заменить ELF на SQLite, то что бы из этого вышло? Можно ли представить всю необходимую информацию в базе данных SQLite? Ответ оказывается положительным, и сделать это на удивление легко.
От чего можно избавитьсяДля исполнения файла SELF требуются две таблицы: self_meta — это заголовок ELF в виде пар «ключ-значение», segments — это загрузочный образ; одна строка — это один заголовок программы; байты находятся в BLOB:
CREATE TABLE segments (
-- исходный индекс phdr
id INTEGER PRIMARY KEY,
-- 'load' | 'tls' | 'stack' | 'relro'
type TEXT NOT NULL,
-- исходное файловое смещение
offset INTEGER NOT NULL,
vaddr INTEGER NOT NULL,
filesz INTEGER NOT NULL,
memsz INTEGER NOT NULL,
r INTEGER, w INTEGER, x INTEGER,
align INTEGER NOT NULL DEFAULT 4096,
-- байты сегмента; NULL для чистого BSS
content BLOB
);Единственная таблица для таблицы символов позволяет избавиться от многих разделов ELF и от индекса .gnu.hash. Это единственная таблица с единственным индексом:
CREATE TABLE symbols (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
-- 'GLIBC_2.2.5'
version TEXT,
value INTEGER,
size INTEGER,
-- 'func' | 'object' | 'tls' | ...
type TEXT,
-- 'global' | 'weak' | 'local'
bind TEXT,
defined INTEGER NOT NULL,
exported INTEGER NOT NULL
);
CREATE INDEX idx_symbols_name ON symbols(name, version);Возможность включения индекса эквивалентна .gnu.hash и .hash в ELF, но это настоящий индекс B-дерева, поддерживаемый SQLite вместо развёрнутого вручную фильтра Блума. [.gnu.hash — это фильтр Блума плюс цепочки корзин, причём он устроен так, что во время поиска символов ld.so может сразу отбросить неподходящий вариант, даже не обращаясь к цепочке.]
Неожиданно, что избавиться можно и от многого другого: .dynstr не нужен, потому что name равно TEXT, а SQLite и так интернирует строки; версионирование символов — это столбец, а не механизм из .gnu.version_r/.gnu.version_d; к тому же не нужна таблица strings.
Для метаданных, как и для инструментария, тоже есть отдельные таблицы: sections, notes, dynamic_entries. Если удалить их, то программа всё равно будет работать; это значит, что strip(1) — это транзакция:
# ldd(1)
sqlite3 hello 'SELECT soname FROM ldd'
libc.so.6
# nm -D --undefined
sqlite3 hello 'SELECT name,version FROM imports LIMIT 3'
__libc_start_main|GLIBC_2.34
_ITM_deregisterTMCloneTable|
puts|GLIBC_2.2.5
# readelf -l
sqlite3 hello \
"SELECT type,vaddr,memsz,r,w,x FROM segments WHERE type='load'"
load|0|1744|1|0|0
load|4096|361|1|0|1
load|8192|312|1|0|0
load|15768|640|1|1|0
# strip(1)
sqlite3 hello 'DELETE FROM sections; DELETE FROM notes; VACUUM;'
# 57344 -> 49152 байт
# всё равно работает: опциональные таблицы были опциональными
./hello
Hello, world!Все инструменты, используемые для чтения файлов ELF, редуцируются до запросов к базе данных. Все инструменты, изменяющие файл ELF, например, strip, могут работать с базой данных в пределах транзакции, а не выполнять тонкие хирургические операции со смещениями: strip — это DELETE и VACUUM. patchelf — это UPDATE.
Любую информацию, отсутствующую в схеме, легко раскрыть при помощи представления базы данных. Например, ldd — это запрос к таблице needed, которая представляет собой объединение таблиц symbols и segments для поиска soname необходимых программе библиотек.
CREATE VIEW exports AS SELECT name, version, type, size FROM symbols WHERE exported = 1;
CREATE VIEW imports AS SELECT name, version FROM symbols WHERE defined = 0;
CREATE VIEW ldd AS SELECT ord, soname FROM needed ORDER BY ord;Как это работает?Для этой цели SQLite резервирует 4-байтный application_id по байтовому смещению 68 заголовка. Мы помечаем его SELF, чтобы обычная база данных SQLite никогда не дала совпадения:
xxd -s 64 -l 8 hello
00000040: 0000 0001 5345 4c46 ....SELFМожно использовать подсистему binfmt_misc, позволяющую вызывать любой двоичный файл так, как будто он нативный. Необходимо только зарегистрировать magic для срабатывания, после чего интерпретатор будет вызывать наш новый файловый формат.
В NixOS регистрация представляет собой несколько строк, соответствующих magic SQLite по смещению 0 и SELF по смещению 68:
boot.binfmt.registrations.self = {
recognitionType = "magic";
offset = 0;
# байты 0-15, 68-71
magicOrExtension = "SQLite format 3\\x00" + ... + "SELF";
# игнорируем середину
mask = "\\xff..\\x00..\\xff";
interpreter = "${self-exec}/bin/self-exec";
};У меня есть небольшой инструмент elf2self, преобразующий файл ELF в файл SELF. Это простой хук postFixup, который можно включить для отдельных пакетов в NixOS. Инструмент считывает ELF, извлекает заголовки программы и таблицу символов, а затем записывает их в базу данных SQLite. Можно подумать о том, чтобы дополнить gcc или ld так, чтобы они напрямую генерировали SELF, но пока мой инструмент остаётся простым способом исследования идеи.

self-exec — это интерпретатор: небольшая программа на C, скомпонованная с libsqlite3. Её реализация очень похожа на реализацию ld.so, но она получает заголовки программы и таблицу символов из базы данных, а не считывает их из файла ELF. Она отображает загружаемые сегменты в память, выполняет их перемещение и передаёт управление точке входа.
Динамическая компоновкаПримечание:
self-execдолжен оставаться файлом ELF. Интерпретатор, который также совпадает с регистрационной записью, сразу уходит в рекурсию и заканчивается ошибкой-ELOOP
Исполнять статическую программу легко, но скучно. Интересной была бы динамическая компоновка, и здесь база данных проявляет себя во всём своём великолепии.
Я исследовал два способа реализации динамической компоновки. Первый заключается в сохранении ld.so и простой замене поиска на запрос SQL при помощи интерфейса rtld-audit glibc, что позволит быстро создавать итерации конструкции. Второй заключается в полной замене ld.so на новый динамический компоновщик, выполняющий все операции поиска и привязки в SQL.
Интерфейс rtld-audit glibc позволяет библиотеке аудита перехватывать операции поиска всех общих объектов (la_objsearch) до выполнения поиска в файловой системе (в том числе и dlopen). После этого библиотека аудита может ответить на вопрос «какая библиотека удовлетворяет этому символу?» при помощи запроса SQL, а не обхода RUNPATH и LD_LIBRARY_PATH. Стандартная ld.so отображает библиотеку в память и выполняет её перемещение, благодаря чему становятся доступны все механизмы glibc: ленивое связывание через PLT, IFUNC, TLS и версионирование символов. При этом само хранилище библиотек организовано как строки, а процесс поиска нужной библиотеки — как выполнение запросов (queries) к этому хранилищу.
# на диске нет библиотеки ELF
rm libgreet.so.1
./app
./app: error while loading shared libraries:
libgreet.so.1: cannot open ...
self scan --db system.db .
SELF_SYSTEM_DB=system.db LD_AUDIT=libself-audit.so ./app
Hello, world, from a SQLite library!Мне было любопытно, как может выглядеть динамический компоновщик целиком на SQL, поэтому я создал прототип. Он называется self-ld: это небольшая программа на C, реализующая динамический компоновщик целиком на SQL. Это proof-of-concept, но он работает. Он отображает сегменты всех объектов, публикует их экспорты, патчит GOT для каждого перемещения и выполняет переход в начало.
SELECT s.value + o.load_bias
FROM relocations r
JOIN symbols s ON r.symbol = s.id
JOIN objects o ON s.object = o.id
WHERE r.id = ?
ORDER BY o.load_order
LIMIT 1;Затраты и бенчмаркКогда меняешь устоявшийся формат на что-то новое, часто важны два аспекта: размер и задержки. Насколько больше файл SELF файла ELF и насколько медленнее он исполняется?
Размер. Файл SELF содержит в себе оверхед B-дерева SQLite, поэтому примерно вдвое больше ELF.

Аналогично двоичным файлам ELF, от бóльшей части оверхеда можно избавиться, потому что в основном это опциональные таблицы для отладки и инструментария. Очистка их от данных и удаление — это транзакция. Очищенный SELF coreutils имеет размер 1 794 048 Б; аналогичный ELF занимает 1 768 632 B, то есть разница в пределах 1%.
Однако ниже мы увидим, что существуют любопытные способы дополнительной амортизации оверхеда, которые я нахожу уникальными и интересными.
Задержки. Я выполнил бенчмарк различных двоичных файлов, от hello на 15 КиБ до gdb на 42 МиБ, компонуемого с 47 библиотеками:

Присутствует фиксированная задержка ~5 мс, связанная с открытием SQLite и запуском интерпретатора, плюс копирование, пропорциональное образу. Это копирование хуже, чем кажется, потому что страницы B-дерева не отображены в память. Два процесса, исполняющих один двоичный файл SELF, не имеют общих текстовых страниц, как это обычно бывает у ELF с mmap, потому что байты копируются из B-дерева, а не отображаются. [Можно заметить, что curl (274 КиБ, 27 библиотек) запускается медленнее, чем ELF git (4,6 МиБ, 5 библиотек). Дело в том, что ld.so выполняет работу пропорционально количеству объектов, а не количеству байт; на это я уже жаловался раньше.]
Однако базе данных SQLite необязательно просто быть одним исполняемым файлом. Она может быть замыканием — единым файлом, содержащим программу и все её транзитивные зависимости. Вывод ldd программы не содержит всю нужную информацию: он создаёт список только soname необходимых библиотек, а не конкретных файлов, удовлетворяющих её потребности. Nix устраняет эту проблему, выполняя в явном виде ресолвинг каждого ребра в конкретный путь хранения при помощи RUNPATH.[Я уже писал о RUNPATH в Nix: например, о том, как сделать его избыточным и ускорить его.]
То же самое мы можем сделать в SELF, сохранив в базу данных каждый путь каждого ребра:
CREATE TABLE objects (id INTEGER PRIMARY KEY, path TEXT UNIQUE,
soname TEXT, kind TEXT, is_root INTEGER);
CREATE TABLE needs (
object_id INTEGER REFERENCES objects(id),
ord INTEGER NOT NULL,
soname TEXT NOT NULL,
-- внешний ключ, устраняющий неопределённость
resolved_path TEXT REFERENCES objects(path)
);self closure упаковывает двоичный файл и его транзитивные зависимости в одну базу данных с указанными рёбрами. Ресолвинг общих библиотек перестаёт быть угадыванием и превращается во внешний ключ, а ldd становится JOIN 🤯:
self closure "$(readlink -f $(command -v ls))" coreutils.db
coreutils.db
sqlite3 -column coreutils.db \
"SELECT n.soname, substr(n.resolved_path, 12, 20)
FROM needs n JOIN objects o ON o.id = n.object_id
WHERE o.is_root = 1"
libgmp.so.10 rfabfsmwq02sn94mb3qg
libacl.so.1 x0zgiss9hdzcsll3cswg
libattr.so.1 08nfpyc4qhzdkc37nznv
libc.so.6 8kvxvr3pmsypxiypq4g8Эта единая база данных представляет собой замыкание исполняемого файла ls и пяти его библиотек: шесть объектов, включая байты сегментов — всё в одном файле размером 4,8 МиБ. Внутри замыкания неоднозначности с soname нет, поскольку замыкание по определению содержит ровно одного поставщика для каждого ребра.
Насколько далеко можно зайти? Один файл, одно пользовательское пространствоЗдесь всё становится по-настоящему интересным: мы можем зайти ещё дальше, упаковав в единую базу данных несколько замыканий.
Я указал для self closure все двоичные файлы ELF в PATH этой системы: 723 исполняемых файлов, подтягивающих четыреста уникальных общих библиотек. 1123 объектов, 346386 символов, 3808 рёбер зависимостей, и всё это в одном файле SQLite.
Оказалось, что если так сделать, то база данных окажется гораздо меньше, чем можно было ожидать.
База данных на 611,9 МиБ по сравнению с файлами ELF на 644,4 МиБ. Всё пользовательское пространство в виде одного файла, к которому можно выполнять запросы, оказалось меньше, чем файлы, из которой она получилась. Затраты на B-дерево, удвоившие размер одного hello, амортизируются для 1123 объектов почти до нуля, и составляют примерно 6% от байт самой программы.
То, как библиотеки и замыкание становятся общими для исполняемых файлов, очень похоже на то, как Nix может делить их между несколькими замыканиями в случае одинакового пути хранения. Если бы каждый корень имел собственное частное замыкание (как в модели AppImage), те же 723 программы занимали 5,53 ГиБ, но дедупликация библиотек и символов логически проистекает из схемы базы данных.
sqlite3 userland.db \
'SELECT count(DISTINCT soname), count(*)
FROM objects WHERE soname IS NOT NULL'
345|399
sqlite3 -column userland.db \
'SELECT soname, count(*) FROM objects
WHERE soname IS NOT NULL
1
ORDER BY 2 DESC LIMIT 4'
libsystemd.so.0 3
libpthread.so.0 3
libgcc_s.so.1 3
libc.so.6 3
sqlite3 userland.db \
"SELECT count(*)
FROM needs
WHERE resolved_path IS NULL AND soname NOT LIKE 'ld-%'"
4Многие общие идиомы, используемые в ELF, естественным образом исключаются из базы данных. Например, LD_PRELOAD — это строка в таблице, а не переменная окружения. Таблица preload — это список объектов, отображаемых последними, поэтому их экспорты побеждают. Таким образом, включение/отключение LD_PRELOAD — это транзакция.
./app.self; echo $?
13
sqlite3 system.db "BEGIN;
"
# тот же двоичный файл, без env var и перекомпоновки
./app.self; echo $?
42
sqlite3 system.db 'DELETE FROM preload;'
./app.self; echo $?
13Нам удалось выполнить атомарный LD_PRELOAD для всего пользовательского пространства в одном файле: «подменить везде malloc на трассирующую версию, а затем выполнить ROLLBACK» в одной транзакции.
Формат завершён и может без потерь выполнять переходы между ELF и SELF. Инструментарий готов: он способен выполнять запросы, вносить модификации и упаковывать замыкания. Писк по SQL идеально работает на основе немодифицированных программ glibc, а загрузчик нативных SQL достаточно хорош, чтобы исследовать новые идеи.
Все наработки выложены на Github. nix run .#self-vm запускает NixOS VM, где hello — это база данных SQLite.
Nix позволяет исследовать подобные радикальные идеи. Если потребуется, мы можем перестроить мир вплоть до ядра Linux. Мы не обязаны ограничиваться существующими решениями и ограничениями прошлого. Можно исследовать новые идеи и смотреть, что из этого выйдет. Надеюсь, эта идея тоже показалась вам интересной.
Реальные исполняемые файлы, к которым можно выполнять запросыМеня не перестаёт удивлять то, как выбор базы данных SQLite в качестве формата файлов позволяет свести всё к SQL. Мне и читателям сразу стала очевидной одна идея: если исполняемый файл — это база данных, а в базу данных возможна запись, то может ли работающая программа хранить в ней своё состояние? 🤔
Да! Можно объединить в один файл не только полный дистрибутив, но и всё состояние всех приложений, снизив потребность в /var/, /tmp/, /home/ и любой другой файловой системы. Программа может при помощи транзакций хранить собственное состояние в том же файле, из которого она запускается.
self-httpd — это proof-of-concept веб-сервера, выполняющего эту задачу. Это программа в едином файле, исполняемая из базы данных. Файл содержит программу, веб-сайт, маршруты и все логи посетителей. Всё состояние обновляется в том же файле SQLite, где находится сама программа.
# Наш сервер - один файл, и это база данных SQLite
file server
server: SQLite 3.x database, application id 1397050438, ...
./server --journal wal 8080
self-httpd: serving 3 routes out of /srv/self/server
self-httpd: listening on http://0.0.0.0:8080 with 4 workers
curl -s localhost:8080 | head -1
<!doctype html>
# пока на этой странице никто не нажимал на кнопку
sqlite3 server 'SELECT count(*) FROM presses'
0
curl -s -X POST -d press localhost:8080/api/press
{"presses":1,"button":"press"}
# данные приложений хранятся в той же базе данных
sqlite3 server 'SELECT id, at, button FROM presses'
1|2026-08-25 03:11:28|press
# как и GET, который изначально запросил страницу
sqlite3 server 'SELECT count(*) AS n, path
FROM visits GROUP BY path'
1|/
1|/api/pressЭтот сервер находится на https://selfdb.exe.xyz. [Простите, если у вас сайт не будет работать, я развернул его на самом дешёвом тарифе.] Это один файл (база данных SQLite) и одновременно сервер. Это веб-сайт, программа, лог посетителей и состояние.
Всё для меня вдохновениеМеня восхищает работа Жюстин Танни, создавшей redbean: веб-сервер в одном файле, собранном в виде Actually Portable Executable с самораспаковывающимся ZIP-архивом.
SELF во многих смыслах менее совершенен. Для решения схожей задачи он использует более простые инструменты, однако меня поразило то, насколько всё сходится к одной теме: SQL.
Для добавления формата архива (ZIP) redbean контейнером становится сама база данных. Redbean предоставляет Lua-хуки для управления ответами, а эквивалентом SELF становится новая строка в таблице handlers.
INSERT INTO handlers VALUES
('/api/busiest', 'SELECT path, count(*)
FROM visits GROUP BY path
ORDER BY 2 DESC LIMIT 5');Если redbean — это Actually Portable Executable, то мой проект — это Actually Queryable Executable. Первый работает где угодно, из другого можно выполнить SELECT.
Как процесс получает доступ к самому себе?
Пока использовать /proc/self/exe невозможно. [Забавно, что мейнтейнер VFS Linux недавно реализовал поддержку в ядре прозрачного binfmt_misc, что позволило бы /proc/self/exe указывать на исходный файл. Я писал об этом.] Когда находится совпадение в binfmt_misc, ядро вообще не исполняет наш файл execve, а исполняет интерпретатор и передаёт ему путь:
self-exec передаёт argv + 1 через программу, поэтому argv[0] программы — это путь к самому исполняемому файлу. Кроме того, перед переходом к точке входа освобождает своё соединение SQLite, поэтому программа может открыть собственный файл и выполнять к нему запросы.
int main(int argc, char **argv) {
sqlite3 *db;
/* файл, который только что исполнило ядро */
sqlite3_open(argv[0], &db);
...
}Можно читать собственную таблицу сегментов или новую таблицу рядом с ней. Записи сохраняются между вызовами.
self-httpdВеб-сервер в нашем примере состоит из трёх таблиц: routes, visits и presses. Мы будем записывать всех посетителей и все нажатия на кнопку.
-- контент, добавляемый к исполняемому файлу
-- после его компиляции и компоновки
CREATE TABLE routes (path TEXT PRIMARY KEY,
mime TEXT, body BLOB);
-- собираемая сайтом информация записывается
-- в исполняемый файл при его работе
CREATE TABLE visits (id INTEGER PRIMARY KEY, at TEXT,
ua TEXT, path TEXT);
CREATE TABLE presses (id INTEGER PRIMARY KEY,
at TEXT, button TEXT);Сборка приложения кажется крайне непримечательной и привычной. Мы исполняем DDL для создания схемы приложения и вставляем веб-сайт при помощи INSERT.
# пока обычный ELF
cc -O2 server.c -o server.elf $(pkg-config --libs sqlite3)
# та же программа в виде строк
elf2self server.elf server
sqlite3 server < site/schema.sql
sqlite3 server "INSERT INTO routes VALUES
('/index.html', 'text/html',
readfile('site/index.html'))"Конвейер ресурсов похож на «обычный веб-сервер», если не считать, что он запрашивает сам у себя контент при помощи SQL. А сам он представляет собой базу данных SQLite.

Страница на https://selfdb.exe.xyz наряду с логом посетителей и нажатиями на кнопку отображает множество любопытной дополнительной информации. Я добавил в неё сегменты, символы и перемещения. Они не встроены на этапе сборки, а получаются при помощи запросов изнутри файла при его исполнении.
Редактирование работающего сайта как транзакцияПосле реализации возможностей транзакций ACID становятся возможными интересные вещи. Веб-сервер может редактировать собственный контент в процессе работы, и эти правки представляют собой транзакции. UPDATE вносится в тот же файл, что и программа, а ROLLBACK откатывает его.
# изменение работающего сайта без перезапуска, перезагрузки и повторного развёртывания
sqlite3 server "UPDATE routes SET body = readfile('new.html')
WHERE path = '/index.html'"
curl -s localhost:8080
<!doctype html><h1>edited in place</h1>Так как в качестве формата файлов выбран SQLite, мы также можем воспользоваться изобилием существующего инструментария. sqldiff сообщает, что конкретно выполнило развёртывание, позволяя проводить аудит и выявлять изменения между двумя версиями одной программы.
sqldiff --summary yesterday.server server
routes: 1 changes, 0 inserts, 0 deletes, 2 unchanged
segments: 0 changes, 0 inserts, 0 deletes, 13 unchanged
symbols: 0 changes, 0 inserts, 0 deletes, 174 unchanged
relocations: 0 changes, 0 inserts, 0 deletes, 99 unchangedА как насчёт полнотекстового поиска? Для добавления FTS5 достаточно CREATE VIRTUAL TABLE, после чего веб-сервер сможет индексировать собственные страницы внутри себя, оставаясь веб-сервером:
sqlite3 server "CREATE VIRTUAL TABLE search USING fts5(path, body);
INSERT INTO search SELECT path, body FROM routes
WHERE mime LIKE 'text/%'"
sqlite3 server "SELECT path, snippet(search, 1, '[', ']', '...', 6)
FROM search WHERE search MATCH 'transaction'"
/index.html|...Editing is a [transaction].</h2>
# по-прежнему работает, только теперь он знает о себе самом
./server 8080Ничего из этого мне писать не пришлось. Это механизмы, уже имеющиеся у SQLite; программа унаследовала их просто потому, что представляет собой базу данных.
Раньше в моде были генераторы статических сайтов, но будущее за actually queryable executable.
Развёртывание — это scp одного файлаМне нравится простота, популяризированная продуктами наподобие exe.dev. Людям часто хочется вернуться в «старое доброе прошлое» с scp и ssh для развёртывания единственного файла, и благодаря формату SELF это снова возможно, но в гораздо лучшем виде! Вместо развёртывания архива PHP мы развёртываем всю систему или замыкание приложений вплоть до libc.
Как выполнить развёртывание, если данные и код переплетены?
Можно воспринимать переразвёртывание как миграцию данных, а для миграции достаточно двух INSERT ... SELECT, потому что программа и её данные находятся в одном файле!
-- развёртывание
ATTACH '/srv/self/server' AS old;
INSERT INTO visits (at, ua, path)
SELECT at, ua, path FROM old.visits;
INSERT INTO presses (at, button)
SELECT at, button FROM old.presses;Заменяем файл, перезапускаемся, и лог посетителей переживёт новую сборку. Можно даже выполнять это для самой программы в обратном порядке. Таблица segments похожа на любую другую таблицу.
На https://selfdb.exe.xyz есть кнопка. При её нажатии выполняется INSERT в исполняемый файл, передавший вам страницу.
Если вам любопытно, код выложен в fzakaria/selfdb. Он определённо сырой и написан с помощью ИИ, но меня это устраивает. Я хотел исследовать эту идею, проверить её реализацию и возможности.
Наверно, я поверхностно затронул лишь часть интересных возможностей. Любопытно было бы увидеть, что с этой системой смогут сделать другие, и мне бы хотелось встретить новые примеры actually queryable executable в реальности. [Мой друг предложил идею поиска через многоадресный DNS для распространения обновлений программ посредством транзакций.]
Как выяснилось, когда мы осознаём, что простая спецификация структуры байт оптимальнее в виде базы данных, многие механизмы, которые мы использовали десятки лет, перестают быть нужными. Программа — это база данных, а база данных — это программа.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Клон SQLite на Rust, воссозданный AI на основе спецификаций | 1 | 10.66 | 23-07-2026 |
| 2 | Как я искал себе читалку и придумал новый формат электронных книг для изучающих язык | 0 | 5.5 | 08-07-2026 |
| 3 | Как не потерять себя в эпоху ИИ | 0 | 8.92 | 01-08-2026 |
| 4 | Как рой ИИ-агентов Cursor создал SQLite с нуля за $1339 | 5 | 7 | 20-07-2026 |
| 5 | Пишу алгоритм FFT на Си для процессора Эльбрус | 0 | 7 | 12-06-2026 |
| 6 | LLM против APK: сколько стоит автономно перепаковать Android-приложение | 0 | 8.81 | 15-07-2026 |
| 7 | YaFF в опенсорсе: как и зачем мы сделали zero‑copy представление для Protobuf | 0 | 6.89 | 17-06-2026 |
| 8 | Книга: «Изучаем SQL за месяц, занимаясь один час в день» | 0 | 7.45 | 02-06-2026 |
| 9 | Странные машины: как хакеры собирают процессор из данных | 0 | 7 | 16-07-2026 |
| 10 | Опубликован Linux From Scratch 13.1 | 0 | 10.6 | 02-09-2026 |