mirror of
https://github.com/stenzek/duckstation.git
synced 2026-07-11 01:24:11 +02:00
Util: Add SQLite helpers and dynamic loading
This commit is contained in:
@@ -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}"))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user