Как связать приложения Rusqlite с пользовательскими сборками SQLite в Windows?

Я хочу использовать специально созданную библиотеку SQLite с привязками Rusqlite при создании приложения базы данных на языке Rust. Меня интересуют как динамические, так и статические варианты связывания. В README Rusqlite указано, что подключение к SQLite в Windows возможно через пакет vcpkg, но не приводятся инструкции (я не хочу использовать vcpkg или любой другой «помощник»).

Веб-сайт Rust предлагает использовать набор инструментов Rust и инструменты сборки Visual Studio C++. Пример «человека» в Rusqlite можно построить с помощью:

{repo-root}> cargo build --example persons --features bundled
or
{repo-root}> cargo build --example persons --features bundled-full

Я могу построить пример «людей» и успешно запустить его (и другие простые примеры) с помощью этих команд. Они создают статически связанный исполняемый файл в «{repo root}\target\debug\examples», используя копию SQLite, включенную в библиотеку.

Команды

{repo-root}> cargo build --example persons
and
{repo-root}> cargo build --example persons --features modern-full

ожидаемо потерпит неудачу из-за отсутствия sqlite3.lib. Какие варианты доступны?


92
1

Ответ:

Решено

Репозиторий Rusqlite включает копию файла объединения SQLite (sqlite3.c) и связанного с ним заголовочного файла (sqlite3.h) внутри каталога "{repo-root}\libsqlite3-sys". Пожалуй, самый простой способ начать тестирование динамической компоновки — скачать x64 «Предварительно скомпилированные двоичные файлы для Windows» и использовать его с проектом Rusqlite.

Предварительно скомпилированный двоичный файл SQLite

Загрузите архив и распакуйте его в каталог «sqlite», расположенный рядом с каталогом «rusqlite», содержащий клонированную/загруженную копию репозитория Rusqlite. Каталог «sqlite» должен содержать два файла: sqlite3.dll и sqlite3.def. Файл sqlite3.lib, необходимый для компоновщика, можно создать из этих двух файлов. Откройте консоль «cmd» с набором среды сборки (Rust/MSBuild), перейдите в каталог «sqlite» и выполните следующую команду:

...sqlite> lib /MACHINE:x64 /DEF:sqlite3.def

который должен создать два новых файла, включая sqlite3.lib. Теперь перейдите в каталог «rusqlite», cd ..\rusqlite. Местоположение «sqlite3.lib» можно передать компоновщику через переменную среды SQLITE3_LIB_DIR. Выполнение

...rusqlite> (set "SQLITE3_LIB_DIR=..\sqlite") && cargo build --example persons
or
...rusqlite> (set "SQLITE3_LIB_DIR=..\sqlite") && cargo build --example persons --features modern-full

должно завершиться успешно и сгенерировать файл "persons.exe" в папке "{repo root}\target\debug\examples". Существует несколько возможных способов проверить результат перед его выполнением. Например, Far Manager имеет плагин ImpEx (также доступен в Far PlugRing), который обеспечивает удобный доступ к исполняемым метаданным. Список элементов верхнего уровня, видимых в ImpEx для «persons.exe», должен, среди прочего, содержать файловый элемент «64BIT» и каталог «Таблица импорта». При открытии последнего должно появиться несколько элементов, похожих на каталоги, названных в честь импортированных файлов DLL, в том числе один для sqlite3.dll.

Также обратите внимание, что размер нового исполняемого файла person.exe значительно меньше, поскольку он больше не интегрирует код библиотеки SQLite. Теперь скопируйте файл sqlite3.dll в каталог, содержащий файл person.exe, например:

...rusqlite> cd target\debug\examples
...examples> copy /Y ..\..\..\..\sqlite\sqlite3.dll .

и запустите файл person.exe, который теперь должен выдавать выходные данные, как и раньше.

Сборка SQLite из исходного кода

Еще одним шагом будет создание библиотеки SQLite из официальной объединенной версии. Удалите ранее созданный каталог «sqlite» и вместо него создайте скрипт sqlite_MSVC_Cpp_Build_Tools_Demo.bat со следующим содержимым:

@echo off


:: ================================ BEGIN MAIN ================================
:MAIN
SetLocal EnableExtensions EnableDelayedExpansion

set ERROR_STATUS=0

set BASEDIR=%~dp0
set BASEDIR=%BASEDIR:~0,-1%
set DISTRODIR=%BASEDIR%\sqlite

call :DOWNLOAD_SQLITE
if %ERROR_STATUS% NEQ 0 exit /b 1
call :EXTRACT_SQLITE
if %ERROR_STATUS% NEQ 0 exit /b 1
if not exist "%DISTRODIR%" (
  echo Distro directory does not exists. Exiting
  exit /b 1
)
call :BUILD_SQLITE
if %ERROR_STATUS% NEQ 0 exit /b 1

EndLocal
exit /b 0
:: ================================= END MAIN =================================


:: ============================================================================
:DOWNLOAD_SQLITE
set YEAR=2024
set VERSION=3460000
set DISTROFILE=sqlite.zip
set URL=https://sqlite.org/%YEAR%/sqlite-amalgamation-%VERSION%.zip

if not exist "%DISTROFILE%" (
  echo ===== Downloading current SQLite release =====
  curl %URL% --output "%DISTROFILE%"
  if %ErrorLevel% EQU 0 (
    echo ----- Downloaded  current SQLite release -----
  ) else (
    set ERROR_STATUS=%ErrorLevel%
    echo Error downloading SQLite distro.
    echo Errod code: !ERROR_STATUS!
  )
) else (
  echo ===== Using previously downloaded SQLite distro =====
)

exit /b %ERROR_STATUS%


:: ============================================================================
:EXTRACT_SQLITE
if not exist "%DISTRODIR%\sqlite3.c" (
  echo ===== Extracting SQLite distro =====
  tar -xf "%DISTROFILE%"
  if %ErrorLevel% EQU 0 (
    move "sqlite-amalgamation-%VERSION%" "%DISTRODIR%"
    echo ----- Extracted  SQLite distro -----
  ) else (
    set ERROR_STATUS=%ErrorLevel%
    echo Error extracting SQLite distro.
    echo Errod code: !ERROR_STATUS!
  )
) else (
  echo ===== Using previously extracted SQLite distro =====
)

exit /b %ERROR_STATUS%


:: ============================================================================
:BUILD_SQLITE
cd /d "%DISTRODIR%"

if not exist sqlite3.lo     (cl -O2 -c sqlite3.c -Fosqlite3.lo)
if not exist libsqlite3.lib (lib sqlite3.lo /OUT:libsqlite3.lib)
if not exist libsqlite3.dmp (dumpbin /ALL libsqlite3.lib /OUT:libsqlite3.dmp)

echo EXPORTS > sqlite3.def
set Command=findstr /XRB /C:"^ *1 sqlite[^ ]* *$" libsqlite3.dmp
for /f "Usebackq tokens=2 delims= " %%I in (`%Command%`) do (
    echo %%I
) 1>>sqlite3.def

lib  /MACHINE:x64 /DEF:sqlite3.def
link /MACHINE:x64 /DEF:sqlite3.def sqlite3.lo /DLL /OUT:sqlite3.dll

exit /b 0

Скрипт относительно небольшой и разбит на функциональные блоки, поэтому я не буду подробно останавливаться на нем (подробности смотрите в коде). При выполнении скрипт загружает копию выпуска SQLite amalgamation , расширяет ее и собирает (среда MSBuild должна быть активирована, как и раньше). Он создает каталог «sqlite» с несколькими файлами, включая sqlite3.dll и sqlite3.lib, которые можно использовать, как и раньше. Этот процесс можно использовать для динамического связывания приложения со специально созданным SQLite, который может интегрировать дополнительные расширения, такие как ICU (см., например, этот проект, в котором основное внимание уделяется инструментальной цепочке MinGW, но также обсуждается среда MSBuild). и предоставляет пользовательские сценарии сборки).

Встраивание кастомного SQLite — взлом процесса сборки Rusqlite

Эта часть, пожалуй, самая сложная, и ее основная цель — скорее исследовательский характер, чем рецепт для повседневного использования. Когда используется один из «связанных» вариантов сборки, процесс сборки Rusqlite, управляемый Cargo, компилирует файл объединения sqlite3.c, включенный в каталог «libsqlite3-sys\sqlite3» репозитория Rusqlite. В принципе, этот файл объединения можно заменить собственной копией, но это самая простая часть. Поскольку процесс сборки SQLite контролируется во время сборки с помощью параметров компилятора, передача этих параметров компилятору C, вызываемому Cargo, имеет важное значение (если только вы не хотите иметь дело со сценарием сборки Rust ("libsqlite3-sys\build.rs"), который помимо этого исследования). Сценарий «libsqlite3-sys\build.rs» принимает параметры конфигурации SQLite «-Dxx» через переменную среды «LIBSQLITE3_FLAGS», но он отклоняет другие типы параметров в этой переменной, например параметры включения. Более того, есть еще варианты связывания, которые, возможно, придется как-то передать.

Например, у меня есть расширенный сценарий (или сценарии, некоторые из которых доступны из соответствующего репозитория и более новый сценарий MSVC, доступный здесь ), который позволяет взломать процесс сборки SQLite. Скрипты не только включают интегрированные расширения SQLite, но также «интегрируют» несколько загружаемых расширений. Среди прочего я интегрирую расширение Zipfile, которое зависит от библиотеки zlib , и включаю расширение ICU, которое зависит от библиотеки ICU . Оба этих расширения требуют флагов компилятора и компоновщика. Мне неизвестно общее решение, но инструменты MSBuild поддерживают специальные переменные среды "CL"/"_CL_" и "LINK"/"_LINK_", которые позволяют передавать необходимые параметры компилятора/компоновщика. Большая часть кода расширенных сценариев ориентирована на создание индивидуального файла объединения. После подготовки это объединение можно скомпилировать в библиотеку dll или использовать для замены объединения, включенного в Rusqlite. Перед вызовом Cargo скрипт устанавливает указанные переменные среды. Соответствующий раздел скрипта:

:: ============================================================================
:RUSQLITE
:: 
:: If RUSQLITE_REPO is set and valid, execute bundled build 
:: 

if not exist "%RUSQLITE_REPO%\libsqlite3-sys\sqlite3\sqlite3.c" (exit /b 0)


echo ========== Building RUSQLITE ===========

cd /d "%RUSQLITE_REPO%\libsqlite3-sys\sqlite3"
if not exist "sqlite3.c.orig" (
    copy /Y "sqlite3.c" "sqlite3.c.orig"
    copy /Y "sqlite3.h" "sqlite3.h.orig"
)
copy /Y "%BINDIR%\src"

if %USE_ZLIB% EQU 1 (
    set ZLIBINCDIR=!DISTRODIR!\compat\zlib
    set ZLIBLIBDIR=!DISTRODIR!\compat\zlib
    set _CL_=!_CL_! "-I%DISTRODIR%\compat\zlib"
    set LINK=!LINK! "/LIBPATH:!ZLIBLIBDIR!"
    set _LINK_=!_LINK_! zdll.lib
)

if %USE_ICU% EQU 1 (
    set _CL_=!_CL_! -DSQLITE_ENABLE_ICU=1 "-I!ICUINCDIR!"
    set LINK=!LINK! "/LIBPATH:!ICULIBDIR!"
    set _LINK_=!_LINK_! icuuc.lib icuin.lib
    set Path=!ICUBINDIR!;!Path!
)

cd /d "%RUSQLITE_REPO%"
set LIBSQLITE3_FLAGS=%EXT_FEATURE_FLAGS%
set SQLITE3_LIB_DIR=%BINDIR%"
::set LINK=!LINK! "/LIBPATH:%BINDIR%"
if not defined EXAMPLE_NAME set EXAMPLE_NAME=intro_sqlite_function_list
rem  --features bundled
call cargo build
call cargo run --example "%EXAMPLE_NAME%"
cd /d "%BASEDIR%"

echo ---------- Built    RUSQLITE -----------

exit /b 0

В дополнение к параметрам компилятора «-D» для каждой связанной библиотеки сценарий передает параметры компилятора «INCLUDE_DIR», «LIB_DIR» и имена файлов *.lib.

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

SELECT name FROM pragma_module_list() ORDER BY name;
SELECT lower('щЩэЭюЮфФ') || upper('щЩэЭюЮфФ');

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

+++++++Стандартная сборка++++++++ +++++++++С дополнительными функциями++++++++++ байт-код csv база данных база данных фсдир фтс3 фтс3 fts3токенизировать fts3токенизировать fts4 fts4 fts4aux fts4aux фтс5 фтс5 fts5vocab fts5vocab генерировать_серию геополия json_each json_each json_tree json_tree pragma_module_list pragma_module_list дерево дерево rtree_i32 rtree_i32 sqlite_dbpage sqlite_stmt table_used zip-файл CI-тест: щЩэЭюЮфФщЩэЭюЮфФ CI-тест: щщээююффЩЭЭЮЮФФ

Честно говоря, в данном конкретном случае это упражнение не увенчалось успехом на 100%. Важно помнить, что статическое связывание — это всего лишь запрос, который может быть выполнен, а может и не быть выполнен. Оказалось, что со всеми добавленными дополнениями, включая три динамически подключаемые библиотеки DLL, статическое связывание SQLite было невозможно. В то же время этот подход может быть полезен для исследовательских сборок сторонних приложений, где функцию rusqlite нельзя легко указать напрямую.

ОБНОВЛЕНИЕ: Создание приложения SQLx.

Мне удалось создать приложение SQLx, следуя последнему подходу, встраивающему собственное объединение SQLite (я добавил соответствующий раздел в расширенный сценарий сборки, упомянутый выше и доступный на GitHub). Интересно, что на этот раз цепочки инструментов Rust/MSBuild сумели выполнить запрос статического связывания с библиотекой SQLite. И скомпилированный демонстрационный проект, и двоичный файл SQLx статически связываются с SQLite, наследуя при этом его зависимости от ICU/zlib.