Util: Add SQLite helpers and dynamic loading

This commit is contained in:
Stenzek
2026-06-06 23:57:32 +10:00
parent 2f3067ab41
commit eae17a4d75
8 changed files with 335 additions and 2 deletions
+3 -1
View File
@@ -64,6 +64,8 @@ find_package(freetype 2.14.3 REQUIRED
NO_DEFAULT_PATH PATHS "${DEPS_PATH}/lib/cmake/freetype")
find_package(harfbuzz REQUIRED
NO_DEFAULT_PATH PATHS "${DEPS_PATH}/lib/cmake/harfbuzz")
find_package(SQLite3 3.53.1 REQUIRED
NO_DEFAULT_PATH PATHS "${DEPS_PATH}/lib/cmake/SQLite3")
find_package(plutosvg 0.0.7 REQUIRED
NO_DEFAULT_PATH PATHS "${DEPS_PATH}/lib/cmake/plutosvg")
find_package(cpuinfo REQUIRED
@@ -94,7 +96,7 @@ else()
endif()
# Verify dependency paths.
foreach(dep zstd WebP PNG libjpeg-turbo freetype harfbuzz plutosvg cpuinfo
foreach(dep zstd WebP PNG libjpeg-turbo freetype harfbuzz SQLite3plutosvg cpuinfo
DiscordRPC SoundTouch libzip Shaderc spirv_cross_c_shared SDL3 Qt6)
if((${dep}_LIBRARY AND NOT "${${dep}_LIBRARY}" MATCHES "^${DEPS_PATH}") OR
(${dep}_DIR AND NOT "${${dep}_DIR}" MATCHES "^${DEPS_PATH}"))
+4 -1
View File
@@ -30,6 +30,7 @@ add_library(util
cue_parser.h
dyn_shaderc.h
dyn_spirv_cross.h
dyn_sqlite.h
elf_file.cpp
elf_file.h
gpu_device.cpp
@@ -81,6 +82,8 @@ add_library(util
sockets.h
spirv_module.cpp
spirv_module.h
sqlite_helpers.cpp
sqlite_helpers.h
state_wrapper.cpp
state_wrapper.h
texture_decompress.cpp
@@ -270,7 +273,7 @@ function(add_util_resources target)
endif()
# Copy dynamically-loaded libraries (harfbuzz/shaderc/spirv-cross) into the bundle.
bundle_libraries(${target} spirv-cross-c-shared Shaderc::shaderc_shared)
bundle_libraries(${target} spirv-cross-c-shared Shaderc::shaderc_shared SQLite3::sqlite3-shared)
if(APPLE)
bundle_libraries(${target} harfbuzz::harfbuzz)
endif()
+37
View File
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include <sqlite3.h>
class Error;
#define DYN_SQLITE_FUNCTIONS(X) \
X(sqlite3_open_v2) \
X(sqlite3_close) \
X(sqlite3_errmsg) \
X(sqlite3_free) \
X(sqlite3_prepare_v2) \
X(sqlite3_finalize) \
X(sqlite3_step) \
X(sqlite3_reset) \
X(sqlite3_exec) \
X(sqlite3_bind_blob) \
X(sqlite3_bind_int) \
X(sqlite3_bind_text) \
X(sqlite3_column_blob) \
X(sqlite3_column_bytes) \
X(sqlite3_column_text) \
X(sqlite3_column_int)
struct DynSqlite
{
#define ADD_FUNC(F) decltype(&::F) F;
DYN_SQLITE_FUNCTIONS(ADD_FUNC)
#undef ADD_FUNC
bool Open(Error* error);
};
extern DynSqlite g_dyn_sqlite;
+200
View File
@@ -0,0 +1,200 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "sqlite_helpers.h"
#include "common/dynamic_library.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include <mutex>
#include <type_traits>
LOG_CHANNEL(Host);
namespace {
struct Locals
{
// Dynamic libraries
DynamicLibrary sqlite_library;
std::once_flag sqlite_init_flag;
};
} // namespace
static Locals s_locals;
static_assert(std::is_trivially_copyable_v<DynSqlite> && std::is_standard_layout_v<DynSqlite>);
DynSqlite g_dyn_sqlite;
bool DynSqlite::Open(Error* error)
{
if (s_locals.sqlite_library.IsOpen())
return true;
std::call_once(s_locals.sqlite_init_flag, [&error]() {
Error lerror;
DynamicLibrary lib;
if (!lib.Open(DynamicLibrary::GetBundledLibraryPath("sqlite3", 3).c_str(), &lerror))
{
ERROR_LOG("Failed to load sqlite: {}", lerror.GetDescription());
Error::SetStringFmt(error, "Failed to load sqlite: {}", lerror.GetDescription());
return;
}
// clang-format off
static const DynamicLibrary::SymbolTable sqlite_symbols[] = {
#define SQLITE_SYMBOL(F) {#F, (void**)&g_dyn_sqlite.F},
DYN_SQLITE_FUNCTIONS(SQLITE_SYMBOL)
#undef SQLITE_SYMBOL
};
if (!lib.ResolveSymbols(sqlite_symbols, std::size(sqlite_symbols), error))
return;
s_locals.sqlite_library = std::move(lib);
});
return s_locals.sqlite_library.IsOpen();
}
sqlite3* SQLiteHelpers::OpenAndCheckDatabase(const char* const path, Error* const error)
{
if (!g_dyn_sqlite.Open(error)) [[unlikely]]
return nullptr;
sqlite3* db = nullptr;
int rc = g_dyn_sqlite.sqlite3_open_v2(path, &db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, nullptr);
// Sanity check on the database.
if (rc == SQLITE_OK)
{
const int check_rc =
g_dyn_sqlite.sqlite3_exec(db, "PRAGMA schema_version;", nullptr, nullptr, nullptr);
if (check_rc != SQLITE_OK)
{
const char* errmsg = g_dyn_sqlite.sqlite3_errmsg(db);
WARNING_LOG("Database {} failed sanity check, rc={}: {}", Path::GetFileName(path), check_rc, errmsg ? errmsg : "<unknown error>");
g_dyn_sqlite.sqlite3_close(db);
rc = SQLITE_NOTADB;
}
}
// If the database is corrupted, then delete it and try again.
if (rc == SQLITE_CORRUPT || rc == SQLITE_NOTADB)
{
WARNING_LOG("Database {} is corrupted, deleting and trying again.", Path::GetFileName(path));
if (Error lerror; !FileSystem::DeleteFile(path, &lerror))
{
ERROR_LOG("Failed to delete corrupted database {}: {}", Path::GetFileName(path), lerror.GetDescription());
}
else
{
rc = g_dyn_sqlite.sqlite3_open_v2(path, &db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, nullptr);
}
}
if (rc != SQLITE_OK)
{
if (db)
g_dyn_sqlite.sqlite3_close(db);
Error::SetStringFmt(error, "sqlite3_open_v2() failed: {}", rc);
return nullptr;
}
return db;
}
void SQLiteHelpers::SetError(Error* const error, sqlite3* const db, std::string_view prefix)
{
const char* errmsg = g_dyn_sqlite.sqlite3_errmsg(db);
if (!prefix.empty())
Error::SetStringFmt(error, "{}{}", prefix, errmsg ? errmsg : "<unknown error>");
else if (errmsg)
Error::SetStringView(error, errmsg ? errmsg : "<unknown error>");
}
bool SQLiteHelpers::Execute(sqlite3* const db, const char* sql, Error* const error /*= nullptr*/)
{
char* errmsg = nullptr;
if (g_dyn_sqlite.sqlite3_exec(db, sql, nullptr, nullptr, &errmsg) == SQLITE_OK)
return true;
Error::SetStringFmt(error, "Failed to execute SQL: {}", errmsg ? errmsg : "<unknown error>");
g_dyn_sqlite.sqlite3_free(errmsg);
return false;
}
bool SQLiteHelpers::BeginTransaction(sqlite3* const db, Error* const error /*= nullptr*/)
{
return Execute(db, "BEGIN TRANSACTION;", error);
}
bool SQLiteHelpers::CommitTransaction(sqlite3* const db, Error* const error /*= nullptr*/)
{
return Execute(db, "COMMIT;", error);
}
void SQLiteHelpers::RollbackTransaction(sqlite3* const db)
{
Execute(db, "ROLLBACK;");
}
SQLitePreparedStatement::SQLitePreparedStatement(SQLitePreparedStatement&& other)
: m_stmt(std::exchange(other.m_stmt, nullptr))
{
}
bool SQLitePreparedStatement::Prepare(sqlite3* const db, const char* sql, Error* const error /*= nullptr*/)
{
if (m_stmt)
{
g_dyn_sqlite.sqlite3_finalize(m_stmt);
m_stmt = nullptr;
}
if (g_dyn_sqlite.sqlite3_prepare_v2(db, sql, -1, &m_stmt, nullptr) != SQLITE_OK)
{
if (error)
Error::SetStringFmt(error, "Failed to prepare statement: {}", g_dyn_sqlite.sqlite3_errmsg(db));
return false;
}
return true;
}
void SQLitePreparedStatement::Destroy()
{
if (m_stmt)
{
g_dyn_sqlite.sqlite3_finalize(m_stmt);
m_stmt = nullptr;
}
}
bool SQLitePreparedStatement::Execute(sqlite3* const db, Error* const error /* = nullptr */)
{
if (g_dyn_sqlite.sqlite3_step(m_stmt) != SQLITE_DONE) [[unlikely]]
{
SQLiteHelpers::SetError(error, db, "Failed to execute statement: ");
return false;
}
return true;
}
SQLitePreparedStatement& SQLitePreparedStatement::operator=(SQLitePreparedStatement&& other)
{
if (this != &other)
{
if (m_stmt)
g_dyn_sqlite.sqlite3_finalize(m_stmt);
m_stmt = std::exchange(other.m_stmt, nullptr);
}
return *this;
}
+84
View File
@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "dyn_sqlite.h"
class Error;
namespace SQLiteHelpers {
sqlite3* OpenAndCheckDatabase(const char* const path, Error* const error);
void SetError(Error* const error, sqlite3* const db, std::string_view prefix = {});
bool Execute(sqlite3* const db, const char* sql, Error* const error = nullptr);
bool BeginTransaction(sqlite3* const db, Error* const error = nullptr);
bool CommitTransaction(sqlite3* const db, Error* const error = nullptr);
void RollbackTransaction(sqlite3* const db);
} // namespace SQLiteHelpers
class SQLitePreparedStatement
{
public:
SQLitePreparedStatement() = default;
~SQLitePreparedStatement()
{
if (m_stmt)
g_dyn_sqlite.sqlite3_finalize(m_stmt);
}
SQLitePreparedStatement(const SQLitePreparedStatement&) = delete;
SQLitePreparedStatement& operator=(const SQLitePreparedStatement&) = delete;
SQLitePreparedStatement(SQLitePreparedStatement&& other);
SQLitePreparedStatement& operator=(SQLitePreparedStatement&& other);
explicit operator bool() const { return (m_stmt != nullptr); }
bool Prepare(sqlite3* const db, const char* sql, Error* const error = nullptr);
void Destroy();
void BindBlob(int idx, const void* data, int size)
{
g_dyn_sqlite.sqlite3_bind_blob(m_stmt, idx, data, size, SQLITE_STATIC);
}
void BindBlob(int idx, const std::span<const u8> data) { BindBlob(idx, data.data(), static_cast<int>(data.size())); }
void BindInt(int idx, int val) { g_dyn_sqlite.sqlite3_bind_int(m_stmt, idx, val); }
void BindText(int idx, const char* text, int size)
{
g_dyn_sqlite.sqlite3_bind_text(m_stmt, idx, text, size, SQLITE_STATIC);
}
void BindText(int idx, std::string_view text) { BindText(idx, text.data(), static_cast<int>(text.size())); }
int Step() { return g_dyn_sqlite.sqlite3_step(m_stmt); }
void Reset() { g_dyn_sqlite.sqlite3_reset(m_stmt); }
bool Execute(sqlite3* const db, Error* const error = nullptr);
const void* ColumnBlob(int idx) const { return g_dyn_sqlite.sqlite3_column_blob(m_stmt, idx); }
std::span<const u8> ColumnBlobBytes(int idx) const
{
const u8* bytes = reinterpret_cast<const u8*>(g_dyn_sqlite.sqlite3_column_blob(m_stmt, idx));
return bytes ? std::span<const u8>(bytes, g_dyn_sqlite.sqlite3_column_bytes(m_stmt, idx)) : std::span<const u8>();
}
int ColumnSizeBytes(int idx) const { return g_dyn_sqlite.sqlite3_column_bytes(m_stmt, idx); }
int ColumnInt(int idx) const { return g_dyn_sqlite.sqlite3_column_int(m_stmt, idx); }
std::string_view ColumnText(int idx) const
{
const char* text = reinterpret_cast<const char*>(g_dyn_sqlite.sqlite3_column_text(m_stmt, idx));
const int size = g_dyn_sqlite.sqlite3_column_bytes(m_stmt, idx);
return text ? std::string_view(text, size) : std::string_view();
}
const char* ColumnTextCStr(int idx) const
{
return reinterpret_cast<const char*>(g_dyn_sqlite.sqlite3_column_text(m_stmt, idx));
}
private:
sqlite3_stmt* m_stmt = nullptr;
};
+1
View File
@@ -46,6 +46,7 @@
<DepsDLLs Include="$(DepsBinDir)shaderc_shared.dll" />
<DepsDLLs Include="$(DepsBinDir)soundtouch.dll" />
<DepsDLLs Include="$(DepsBinDir)spirv-cross-c-shared.dll" />
<DepsDLLs Include="$(DepsBinDir)sqlite3.dll" />
<DepsDLLs Include="$(DepsBinDir)zip.dll" />
<DepsDLLs Include="$(DepsBinDir)zlib1.dll" />
<DepsDLLs Include="$(DepsBinDir)zstd.dll" />
+3
View File
@@ -3,6 +3,7 @@
<Import Project="..\..\dep\vsprops\Configurations.props" />
<ItemGroup>
<ClInclude Include="animated_image.h" />
<ClInclude Include="dyn_sqlite.h" />
<ClInclude Include="http_cache.h" />
<ClInclude Include="object_archive.h" />
<ClInclude Include="compress_helpers.h" />
@@ -97,6 +98,7 @@
<ClInclude Include="shiftjis.h" />
<ClInclude Include="sockets.h" />
<ClInclude Include="spirv_module.h" />
<ClInclude Include="sqlite_helpers.h" />
<ClInclude Include="state_wrapper.h" />
<ClInclude Include="texture_decompress.h" />
<ClInclude Include="translation.h" />
@@ -205,6 +207,7 @@
<ClCompile Include="page_fault_handler.cpp" />
<ClCompile Include="sockets.cpp" />
<ClCompile Include="spirv_module.cpp" />
<ClCompile Include="sqlite_helpers.cpp" />
<ClCompile Include="state_wrapper.cpp" />
<ClCompile Include="texture_decompress.cpp" />
<ClCompile Include="translation.cpp" />
+3
View File
@@ -84,6 +84,8 @@
<ClInclude Include="sdl_video_helpers.h" />
<ClInclude Include="object_archive.h" />
<ClInclude Include="http_cache.h" />
<ClInclude Include="dyn_sqlite.h" />
<ClInclude Include="sqlite_helpers.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="state_wrapper.cpp" />
@@ -170,6 +172,7 @@
<ClCompile Include="object_archive.cpp" />
<ClCompile Include="http_cache.cpp" />
<ClCompile Include="xaudio2_audio_stream.cpp" />
<ClCompile Include="sqlite_helpers.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="metal_shaders.metal" />