Последние несколько лет я, похоже, одержим двумя темами: а) 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

Я разработал достаточно функциональный прототип, который назвал SELFStructured Executable & Linkable Format. Если вам любопытно, его можно изучить на GitHub. Меня приятно удивили любопытные последствия этой идеи.

ELF — база данных, которая отказывается это признавать

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

Механизм ELF

Примитив баз данных, который он переизобрёл

.strtab / .dynstr

интернирование строк

.hash / .gnu.hash

индекс (CREATE INDEX)

таблица заголовков разделов

sqlite_schema, таблица таблиц

st_name → смещение в .strtab

реализуемый вручную внешний ключ

sh_offset / sh_size

структура записи страницы B-дерева

.gnu.version_r

столбец

objcopy --strip-debug

DELETE + VACUUM

кэш ldconfigdebuginfod

индексы 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 и VACUUMpatchelf — это 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 достаточно хорош, чтобы исследовать новые идеи.

Все наработки выложены на Githubnix 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) и одновременно сервер. Это веб-сайт, программа, лог посетителей и состояние.

Screenshot of selfdb.exe.xyz. The heading reads "This page is a row in the executable that served it." Below it a console block shows `file server` reporting a SQLite database with application id 1397050438, and `xxd` showing the bytes "....SELF" at offset 68. Under the heading "What is in it, right now" is a grid of live counters read out of the file while it answered the request: 13 segments, 179 symbols, 105 relocations, 2 needed libraries, 3 routes, 12 tables, 103 visits recorded, 24 presses recorded.

Всё для меня вдохновение

Меня восхищает работа Жюстин Танни, создавшей 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.

Всё, что нужно — это 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;
	/* файл, который только что исполнило ядро */
	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 для распространения обновлений программ посредством транзакций.]

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