Статья «Ваш исполняемый файл — это база данных SQLite» вызвала неожиданно живой отклик. Идея формата, о котором автор давно размышлял, нашла отклик и у других разработчиков.

Кратко напомним суть: SELF — формат, в котором сама программа является базой данных SQLite. С помощью binfmt_misc можно запустить кастомный интерпретатор, который сопоставляет строки таблицы segments и переходит на точку входа — и целый класс инструментов для работы с бинарниками сворачивается до SQL-запросов.
Что продолжает удивлять — то, насколько сильно факт хранения программы в 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.
Там, где redbean нуждается в формате архива (ZIP), сама база данных выступает контейнером. 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.
Всё, что нужно — argv[0]
Как процесс получает доступ к самому себе? 🤔
Пока что нельзя использовать /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;
/* the file the kernel just executed */
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 его отменяет.
# меняем работающий сайт. без рестарта, без reload, без деплоя
$ 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, и программа получает её бесплатно просто потому, что является базой данных.
Раньше все увлекались статическими генераторами сайтов, но будущее — за действительно запрашиваемым исполняемым файлом.
Деплой — это 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, если интересно. Он, вероятно, немного сыроват и точно писался с помощью AI, но это нормально — цель была исследовать идею и проверить её осуществимость.
Это лишь верхушка возможных интересных применений. Интересно, что сделают с этим другие, и хотелось бы увидеть больше примеров «действительно запрашиваемых исполняемых файлов» в реальном мире.
Один друг предложил идею обнаружения через multicast DNS для распространения обновлений программы транзакциями.
Оказывается, когда переосмысляешь то, что раньше считалось просто спецификацией раскладки байтов, и превращаешь это в базу данных, множество механизмов, которыми пользовались десятилетиями, просто перестают быть нужны. Программа — это база данных, а база данных — это программа.
«Никогда, никогда не стоит недооценивать важность получения удовольствия»
— Рэнди Пауш