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

мем с Дэнни из Ted Lasso, говорящим sqlite is life

Кратко напомним суть: 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, и он же сервер. Это одновременно сайт, программа, лог посетителей и состояние.

Скриншот selfdb.exe.xyz с заголовком: эта страница — строка в исполняемом файле, который её отдал. Показаны счётчики сегментов, символов, релокаций, маршрутов, таблиц, визитов и нажатий кнопки

Всё — источник вдохновения

Большое уважение вызывает работа Джастин Танни, чей проект 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 для распространения обновлений программы транзакциями.

Оказывается, когда переосмысляешь то, что раньше считалось просто спецификацией раскладки байтов, и превращаешь это в базу данных, множество механизмов, которыми пользовались десятилетиями, просто перестают быть нужны. Программа — это база данных, а база данных — это программа.

«Никогда, никогда не стоит недооценивать важность получения удовольствия»

— Рэнди Пауш