Загружаемые расширения SQLite: подключение и программирование


SQLite может загружать расширения во время работы без необходимости в повторной компиляции и перезапуске приложения, что позволяет разрабатывать и тестировать новые SQL-функции, последовательности сортировки, виртуальные таблицы и VFS, а также подключать их по мере необходимости

Вся рубрика SQLite: уроки, инструменты и примеры

Расширения также можно компоновать статически вместе с приложением. Код, приведённый ниже, подходит как для статически скомпонованных расширений, так и для расширений, загружаемых во время выполнения. Однако функции точки входа (sqlite3_extension_init) лучше переименовать, чтобы избежать конфликтов имён, если в приложении используется несколько расширений

Как загрузить расширение SQLite во время выполнения

Расширение SQLite — это разделяемая библиотека (shared library / DLL). Чтобы загрузить его, нужно передать SQLite имя файла, содержащего разделяемую библиотеку, и точку входа для инициализации расширения. В коде на C эта информация передаётся через API sqlite3_load_extension()

Разные операционные системы используют разные суффиксы имен файлов для разделяемых библиотек: Windows — «.dll», Mac — «.dylib», а большинство систем Unix, кроме Mac, — «.so». Если требуется переносимый код, следует опустить суффикс в названии разделяемой библиотеки, так как интерфейс sqlite3_load_extension() автоматически добавит нужный суффикс

Существует также SQL-функция для загрузки расширений: load_extension(X,Y). Она работает точно так же, как C-интерфейс sqlite3_load_extension()

Оба метода загрузки расширений позволяют указать имя точки входа. Этот аргумент можно оставить пустым, передав указатель NULL для C-интерфейса sqlite3_load_extension() или опустив второй аргумент для SQL-интерфейса load_extension(). В этом случае система попытается самостоятельно определить точку входа, сначала используя глобальное имя sqlite3_extension_init

Если это не сработает, она сгенерирует имя по шаблону sqlite3_X_init, где X — строчное представление каждого ASCII-символа в имени файла после последнего / и до первой следующей ., причем первые три символа опускаются, если это lib

По умолчанию загрузка расширений отключена для повышения безопасности. Чтобы активировать загрузку расширений на C или через SQL, необходимо включить эту функцию, используя C-API sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL) в вашем приложении

Из командной строки расширения можно загружать с помощью dot-команды .load. Например:

.load ./YourCode

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

Команда .load с одним аргументом вызывает sqlite3_load_extension(), устанавливая параметр zProc в NULL. Это заставляет SQLite сначала пытаться найти точку входа sqlite3_extension_init, а затем sqlite3_X_init, где X будет взято из имени файла. Если у расширения есть точка входа с другим именем, её следует указать в качестве второго аргумента

.load ./YourCode nonstandard_entry_point

По этой теме полезно отдельно посмотреть EXPLAIN QUERY PLAN: план выполнения SQL-запроса в SQLite, чтобы расширить контекст и сравнить подходы

По этой теме полезно отдельно посмотреть Создание Flutter-приложения с SQLite, BLoC и Streams, чтобы расширить контекст и сравнить подходы

Компиляция загружаемого расширения SQLite под Unix, Mac и Windows

Загружаемые расширения — это код на C. Для их компиляции в большинстве Unix-подобных операционных систем обычно используется команда следующего вида:

gcc -g -fPIC -shared YourCode.c -o YourCode.so

Mac является Unix-подобной системой, однако не следует обычным соглашениям о разделяемых библиотеках. Для компиляции разделяемой библиотеки на Mac используйте команду следующего вида:

gcc -g -fPIC -dynamiclib YourCode.c -o YourCode.dylib

Если при попытке загрузить библиотеку появляется сообщение об ошибке mach-o, but wrong architecture, возможно, потребуется добавить параметры командной строки -arch i386 или -arch x86_64 к gcc, в зависимости от того, как собрано приложение

Для компиляции в Windows с использованием MSVC обычно подходит команда, аналогичная следующей:

cl YourCode.c -link -dll -out:YourCode.dll

Для компиляции под Windows с использованием MinGW командная строка выглядит так же, как для Unix, за исключением того, что суффикс выходного файла меняется на .dll, а аргумент -fPIC опускается:

gcc -g -shared YourCode.c -o YourCode.dll

Разработка загружаемых расширений SQLite на C

Шаблонное загружаемое расширение содержит три обязательных элемента

Используйте #include <sqlite3ext.h> в начале файлов исходного кода вместо #include <sqlite3.h>

Поместите макрос SQLITE_EXTENSION_INIT1 на отдельную строку сразу после строки #include <sqlite3ext.h>

Добавьте процедуру точки входа загрузки расширения, которая выглядит примерно следующим образом:

#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_extension_init( /* <== Возможно, измените это имя */ sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi
){ int rc = SQLITE_OK; SQLITE_EXTENSION_INIT2(pApi); /* вставьте здесь код инициализации вашего расширения */ return rc;
}

Я рекомендую настраивать имя точки входа в соответствии с именем генерируемой разделяемой библиотеки, а не использовать универсальное имя sqlite3_extension_init

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

Если разделяемая библиотека в итоге будет называться YourCode.so, YourCode.dll или YourCode.dylib, то правильным именем точки входа будет sqlite3_yourcode_init

Примеры расширений

Множество примеров полных и работающих загружаемых расширений можно найти в дереве исходного кода SQLite в подкаталоге ext/misc. Каждый файл в этом каталоге является отдельным расширением. Документация предоставляется в виде заголовочного комментария в файле. Ниже приведены краткие сведения о нескольких расширениях из подкаталога ext/misc:

  • carray.c — реализация табличной функции carray.
  • compress.c — реализация определяемых приложением SQL-функций compress() и uncompress(), выполняющих zLib-сжатие текстового или blob-содержимого.
  • rot13.c — реализация SQL-функции rot13(). Это очень простой пример функции-расширения, полезный в качестве шаблона для создания новых расширений.
  • series.c — реализация виртуальной таблицы generate_series и табличной функции. Это относительно простой пример реализации виртуальной таблицы, который может служить шаблоном для написания новых виртуальных таблиц.

Другие, более сложные расширения можно найти в подпапках под ext/, отличных от ext/misc/

Постоянные загружаемые расширения

По умолчанию загружаемое расширение выгружается из памяти процесса при закрытии соединения с базой данных, которое первоначально вызвало sqlite3_load_extension(). Иными словами, метод xDlClose объекта sqlite3_vfs вызывается для всех расширений при закрытии соединения с базой данных

Однако если процедура инициализации возвращает SQLITE_OK_LOAD_PERMANENTLY вместо SQLITE_OK, расширение не будет выгружено (xDlClose не будет вызван) и останется в памяти процесса на неопределённый срок. Возвращаемое значение SQLITE_OK_LOAD_PERMANENTLY полезно для расширений, которые хотят зарегистрировать новые VFS

Важно понимать разницу: расширение, функция инициализации которого возвращает SQLITE_OK_LOAD_PERMANENTLY, продолжает существовать в памяти после закрытия соединения с базой данных, однако не регистрируется автоматически в последующих соединениях. Это позволяет загружать расширения, реализующие новые VFS

Для постоянной загрузки и регистрации расширения, реализующего новые SQL-функции, последовательности сортировки и/или виртуальные таблицы таким образом, чтобы добавленные возможности были доступны всем последующим соединениям с базой данных, процедура инициализации должна также вызывать sqlite3_auto_extension() для подфункции, которая будет регистрировать эти службы

Расширение vfsstat.c демонстрирует пример загружаемого расширения, которое постоянно регистрирует как новый VFS, так и новую виртуальную таблицу. Процедура инициализации sqlite3_vfsstat_init() в этом расширении вызывается только один раз — при первой загрузке расширения

Она регистрирует новый VFS vfslog только один раз и возвращает SQLITE_OK_LOAD_PERMANENTLY, чтобы код, используемый для реализации VFS vfslog, оставался в памяти

Процедура инициализации также вызывает sqlite3_auto_extension() с указателем на функцию vstatRegister(), чтобы все последующие соединения с базой данных вызывали функцию vstatRegister() при запуске и тем самым регистрировали виртуальную таблицу vfsstat

Статическая компоновка расширения

Один и тот же исходный код может использоваться как для разделяемой библиотеки или DLL, загружаемой во время выполнения, так и в качестве модуля, статически скомпонованного с приложением. Это обеспечивает гибкость и позволяет повторно использовать один и тот же код различными способами

Для статической компоновки расширения достаточно добавить параметр компиляции -DSQLITE_CORE. Макрос SQLITE_CORE превращает макросы SQLITE_EXTENSION_INIT1 и SQLITE_EXTENSION_INIT2 в пустые операции (no-ops). Затем нужно изменить приложение так, чтобы оно вызывало точку входа напрямую, передавая указатель NULL в качестве третьего параметра pApi

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

На мой взгляд, это одна из тех деталей, которую легко упустить при первом переходе от динамической компоновки к статической — и которая стоит потраченного на неё времени при проектировании структуры расширений заранее

Если в приложении будет открываться несколько соединений с базой данных, вместо того чтобы вызывать точки входа расширений для каждого соединения отдельно, стоит рассмотреть использование интерфейса sqlite3_auto_extension() для регистрации расширений и их автоматического запуска при открытии каждого соединения с базой данных

Каждое расширение нужно зарегистрировать только один раз, и это можно сделать в начале процедуры main(). Использование интерфейса sqlite3_auto_extension() для регистрации расширений делает их работу такой, как если бы они были встроены в ядро SQLite — они автоматически присутствуют при каждом открытии нового соединения с базой данных без необходимости дополнительной инициализации

Нужно только убедиться, что все необходимые настройки с помощью sqlite3_config() завершены до регистрации расширений, поскольку интерфейс sqlite3_auto_extension() неявно вызывает sqlite3_initialize()

Детали реализации

SQLite реализует загрузку расширений во время выполнения с помощью методов xDlOpen(), xDlError(), xDlSym() и xDlClose() объекта sqlite3_vfs. Эти методы реализованы с использованием библиотеки dlopen() в Unix — что объясняет, почему SQLite обычно требует компоновки с библиотекой -ldl в Unix-системах — и с использованием API LoadLibrary() в Windows

В пользовательском VFS для нестандартных систем все эти методы могут быть опущены; в этом случае механизм загрузки расширений во время выполнения работать не будет, хотя статическая компоновка кода расширения по-прежнему будет возможна при условии уникальных имён точек входа. SQLite можно скомпилировать с параметром SQLITE_OMIT_LOAD_EXTENSION, чтобы полностью исключить код загрузки расширений из сборки

На мой взгляд, понимание этого слоя реализации полезно при отладке проблем с загрузкой на нестандартных платформах: если методы xDl* не реализованы в используемом VFS, никакие вызовы sqlite3_load_extension() не дадут результата, и ошибка будет именно на этом уровне, а не в коде самого расширения

Часто задаваемые вопросы

Почему sqlite3_load_extension() возвращает ошибку, хотя файл библиотеки существует?

Скорее всего, загрузка расширений не включена. Перед вызовом sqlite3_load_extension() необходимо вызвать sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL). По умолчанию загрузка расширений отключена из соображений безопасности

Можно ли использовать один и тот же исходный файл расширения и для динамической, и для статической компоновки?

Да. Для статической компоновки достаточно добавить флаг компиляции -DSQLITE_CORE — макросы SQLITE_EXTENSION_INIT1 и SQLITE_EXTENSION_INIT2 превратятся в пустые операции, и код скомпилируется как обычный модуль приложения

Как сделать так, чтобы расширение было доступно во всех соединениях с базой данных, а не только в том, через которое оно было загружено?

Нужно вызвать sqlite3_auto_extension() с указателем на функцию регистрации внутри процедуры инициализации расширения. Тогда при открытии каждого нового соединения SQLite будет автоматически вызывать эту функцию

Что происходит с расширением при закрытии соединения с базой данных?

По умолчанию расширение выгружается из памяти вместе с закрытием соединения. Если процедура инициализации возвращает SQLITE_OK_LOAD_PERMANENTLY, расширение остаётся в памяти процесса, но не регистрируется автоматически в новых соединениях

Как правильно назвать точку входа расширения?

Рекомендуется использовать шаблон sqlite3_X_init, где X — строчное имя файла библиотеки без суффикса и префикса lib. Например, для библиотеки YourCode.so правильное имя точки входа — sqlite3_yourcode_init. Это позволяет избежать конфликтов имён при статической компоновке нескольких расширений

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x