Я хочу использовать специально созданную библиотеку 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. Какие варианты доступны?
Репозиторий Rusqlite включает копию файла объединения SQLite (sqlite3.c) и связанного с ним заголовочного файла (sqlite3.h) внутри каталога "{repo-root}\libsqlite3-sys". Пожалуй, самый простой способ начать тестирование динамической компоновки — скачать x64 «Предварительно скомпилированные двоичные файлы для Windows» и использовать его с проектом Rusqlite.
Загрузите архив и распакуйте его в каталог «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_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). и предоставляет пользовательские сценарии сборки).
Эта часть, пожалуй, самая сложная, и ее основная цель — скорее исследовательский характер, чем рецепт для повседневного использования. Когда используется один из «связанных» вариантов сборки, процесс сборки 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 (в данном случае кириллицей). Ниже приведены выходные данные этого примера, скомпилированные с дополнительными функциями и без них (выровненные вручную).
Честно говоря, в данном конкретном случае это упражнение не увенчалось успехом на 100%. Важно помнить, что статическое связывание — это всего лишь запрос, который может быть выполнен, а может и не быть выполнен. Оказалось, что со всеми добавленными дополнениями, включая три динамически подключаемые библиотеки DLL, статическое связывание SQLite было невозможно. В то же время этот подход может быть полезен для исследовательских сборок сторонних приложений, где функцию rusqlite нельзя легко указать напрямую.
Мне удалось создать приложение SQLx, следуя последнему подходу, встраивающему собственное объединение SQLite (я добавил соответствующий раздел в расширенный сценарий сборки, упомянутый выше и доступный на GitHub). Интересно, что на этот раз цепочки инструментов Rust/MSBuild сумели выполнить запрос статического связывания с библиотекой SQLite. И скомпилированный демонстрационный проект, и двоичный файл SQLx статически связываются с SQLite, наследуя при этом его зависимости от ICU/zlib.