From 7e1a12b8f707d1f0d64c31eaf655bc3cee4e27df Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 7 Apr 2026 05:45:16 +0000 Subject: [PATCH] feat: add generic plugin ABI and loader --- include/ProxySQL_Plugin.h | 82 +++++++++++ include/ProxySQL_PluginManager.h | 38 +++++ lib/Makefile | 1 + lib/ProxySQL_PluginManager.cpp | 131 ++++++++++++++++++ test/tap/test_helpers/fake_plugin.cpp | 36 +++++ test/tap/tests/unit/Makefile | 22 ++- test/tap/tests/unit/plugin_manager_unit-t.cpp | 24 ++++ 7 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 include/ProxySQL_Plugin.h create mode 100644 include/ProxySQL_PluginManager.h create mode 100644 lib/ProxySQL_PluginManager.cpp create mode 100644 test/tap/test_helpers/fake_plugin.cpp create mode 100644 test/tap/tests/unit/plugin_manager_unit-t.cpp diff --git a/include/ProxySQL_Plugin.h b/include/ProxySQL_Plugin.h new file mode 100644 index 000000000..d7933c80b --- /dev/null +++ b/include/ProxySQL_Plugin.h @@ -0,0 +1,82 @@ +#ifndef PROXYSQL_PLUGIN_H +#define PROXYSQL_PLUGIN_H + +#include +#include +#include + +enum ProxySQL_PluginDBKind { + admin_db, + config_db, + stats_db, +}; + +struct ProxySQL_PluginTableDef { + ProxySQL_PluginDBKind db_kind{admin_db}; + const char *schema_name{nullptr}; + const char *table_name{nullptr}; + const char *create_statement{nullptr}; +}; + +struct ProxySQL_PluginCommandContext { + const char *command_name{nullptr}; + std::vector arguments; +}; + +struct ProxySQL_PluginCommandResult { + bool success{true}; + std::string message; + std::vector> rows; +}; + +class ProxySQL_PluginServices; + +using proxysql_plugin_admin_command_cb = + bool (*)(const ProxySQL_PluginCommandContext &, ProxySQL_PluginCommandResult &, void *); + +using proxysql_plugin_register_table_cb = + bool (*)(void *, const ProxySQL_PluginTableDef &); + +using proxysql_plugin_register_command_cb = + bool (*)(void *, const char *, proxysql_plugin_admin_command_cb, void *); + +using proxysql_plugin_snapshot_cb = + bool (*)(void *, std::vector> &); + +using proxysql_plugin_log_message_cb = + void (*)(void *, int, const char *); + +struct ProxySQL_PluginServices { + proxysql_plugin_register_table_cb register_table{nullptr}; + proxysql_plugin_register_command_cb register_command{nullptr}; + proxysql_plugin_snapshot_cb get_mysql_users_snapshot{nullptr}; + proxysql_plugin_snapshot_cb get_mysql_servers_snapshot{nullptr}; + proxysql_plugin_snapshot_cb get_mysql_group_replication_hostgroups_snapshot{nullptr}; + proxysql_plugin_log_message_cb log_message{nullptr}; + void *context{nullptr}; +}; + +using proxysql_plugin_init_cb = + bool (*)(ProxySQL_PluginServices *, std::string &); + +using proxysql_plugin_start_cb = + bool (*)(std::string &); + +using proxysql_plugin_stop_cb = + bool (*)(); + +using proxysql_plugin_status_cb = + bool (*)(std::string &); + +struct ProxySQL_PluginDescriptor { + uint32_t abi_version{0}; + const char *name{nullptr}; + proxysql_plugin_init_cb init{nullptr}; + proxysql_plugin_start_cb start{nullptr}; + proxysql_plugin_stop_cb stop{nullptr}; + proxysql_plugin_status_cb status{nullptr}; +}; + +using proxysql_plugin_descriptor_v1_t = const ProxySQL_PluginDescriptor *(*)(); + +#endif /* PROXYSQL_PLUGIN_H */ diff --git a/include/ProxySQL_PluginManager.h b/include/ProxySQL_PluginManager.h new file mode 100644 index 000000000..aecdc2cdd --- /dev/null +++ b/include/ProxySQL_PluginManager.h @@ -0,0 +1,38 @@ +#ifndef PROXYSQL_PLUGIN_MANAGER_H +#define PROXYSQL_PLUGIN_MANAGER_H + +#include "ProxySQL_Plugin.h" + +#include +#include +#include + +class ProxySQL_PluginManager { +public: + ProxySQL_PluginManager(); + ~ProxySQL_PluginManager(); + + ProxySQL_PluginManager(const ProxySQL_PluginManager &) = delete; + ProxySQL_PluginManager &operator=(const ProxySQL_PluginManager &) = delete; + + bool load(const std::string &path, std::string &err); + bool init_all(std::string &err); + bool start_all(std::string &err); + bool stop_all(); + + size_t size() const; + +private: + struct plugin_handle_t { + void *handle{nullptr}; + const ProxySQL_PluginDescriptor *descriptor{nullptr}; + bool initialized{false}; + bool started{false}; + bool stopped{false}; + }; + + std::vector plugins_; + ProxySQL_PluginServices services_; +}; + +#endif /* PROXYSQL_PLUGIN_MANAGER_H */ diff --git a/lib/Makefile b/lib/Makefile index 23eb8984b..5f59672b0 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -90,6 +90,7 @@ default: libproxysql.a _OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo SpookyV2.oo MySQL_Authentication.oo gen_utils.oo sqlite3db.oo mysql_connection.oo MySQL_HostGroups_Manager.oo mysql_data_stream.oo MySQL_Thread.oo MySQL_Session.oo MySQL_Protocol.oo mysql_backend.oo Query_Processor.oo MySQL_Query_Processor.oo PgSQL_Query_Processor.oo ProxySQL_Admin.oo ProxySQL_Config.oo ProxySQL_Restapi.oo MySQL_Monitor.oo MySQL_Logger.oo log_utils.oo thread.oo MySQL_PreparedStatement.oo ProxySQL_Cluster.oo ClickHouse_Authentication.oo ClickHouse_Server.oo ProxySQL_Statistics.oo Chart_bundle_js.oo ProxySQL_HTTP_Server.oo ProxySQL_RESTAPI_Server.oo font-awesome.min.css.oo main-bundle.min.css.oo MySQL_Variables.oo c_tokenizer.oo proxysql_utils.oo proxysql_coredump.oo proxysql_sslkeylog.oo \ sha256crypt.oo \ + ProxySQL_PluginManager.oo \ BaseSrvList.oo BaseHGC.oo Base_HostGroups_Manager.oo \ QP_rule_text.oo QP_query_digest_stats.oo \ GTID_Server_Data.oo MyHGC.oo MySrvConnList.oo MySrvC.oo \ diff --git a/lib/ProxySQL_PluginManager.cpp b/lib/ProxySQL_PluginManager.cpp new file mode 100644 index 000000000..943349443 --- /dev/null +++ b/lib/ProxySQL_PluginManager.cpp @@ -0,0 +1,131 @@ +#include "ProxySQL_PluginManager.h" + +#include + +#include + +namespace { + +std::string format_dl_error(const char *prefix) { + const char *dl_err = dlerror(); + if (dl_err == nullptr) { + return prefix; + } + return std::string(prefix) + dl_err; +} + +} // namespace + +ProxySQL_PluginManager::ProxySQL_PluginManager() : services_{} {} + +ProxySQL_PluginManager::~ProxySQL_PluginManager() { + stop_all(); + for (auto it = plugins_.rbegin(); it != plugins_.rend(); ++it) { + if (it->handle != nullptr) { + dlclose(it->handle); + it->handle = nullptr; + } + } +} + +bool ProxySQL_PluginManager::load(const std::string &path, std::string &err) { + err.clear(); + + void *handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) { + err = format_dl_error("dlopen failed: "); + return false; + } + + dlerror(); + auto descriptor_fn = reinterpret_cast( + dlsym(handle, "proxysql_plugin_descriptor_v1")); + const char *dlsym_err = dlerror(); + if (dlsym_err != nullptr || descriptor_fn == nullptr) { + err = dlsym_err != nullptr ? dlsym_err : "missing proxysql_plugin_descriptor_v1"; + dlclose(handle); + return false; + } + + const ProxySQL_PluginDescriptor *descriptor = descriptor_fn(); + if (descriptor == nullptr) { + err = "proxysql_plugin_descriptor_v1 returned null"; + dlclose(handle); + return false; + } + + if (descriptor->abi_version != 1) { + err = "unsupported plugin ABI version"; + dlclose(handle); + return false; + } + + plugin_handle_t plugin; + plugin.handle = handle; + plugin.descriptor = descriptor; + plugins_.push_back(plugin); + return true; +} + +bool ProxySQL_PluginManager::init_all(std::string &err) { + err.clear(); + + for (auto &plugin : plugins_) { + if (plugin.initialized || plugin.stopped) { + continue; + } + if (plugin.descriptor == nullptr || plugin.descriptor->init == nullptr) { + plugin.initialized = true; + continue; + } + if (!plugin.descriptor->init(&services_, err)) { + return false; + } + plugin.initialized = true; + } + + return true; +} + +bool ProxySQL_PluginManager::start_all(std::string &err) { + err.clear(); + + for (auto &plugin : plugins_) { + if (plugin.started || plugin.stopped) { + continue; + } + if (plugin.descriptor == nullptr || plugin.descriptor->start == nullptr) { + plugin.started = true; + continue; + } + if (!plugin.descriptor->start(err)) { + return false; + } + plugin.started = true; + } + + return true; +} + +bool ProxySQL_PluginManager::stop_all() { + bool ok = true; + + for (auto it = plugins_.rbegin(); it != plugins_.rend(); ++it) { + if (!it->initialized && !it->started) { + continue; + } + if (it->stopped) { + continue; + } + if (it->descriptor != nullptr && it->descriptor->stop != nullptr) { + ok = it->descriptor->stop() && ok; + } + it->stopped = true; + } + + return ok; +} + +size_t ProxySQL_PluginManager::size() const { + return plugins_.size(); +} diff --git a/test/tap/test_helpers/fake_plugin.cpp b/test/tap/test_helpers/fake_plugin.cpp new file mode 100644 index 000000000..78b4a7d76 --- /dev/null +++ b/test/tap/test_helpers/fake_plugin.cpp @@ -0,0 +1,36 @@ +#include "ProxySQL_Plugin.h" + +#include + +namespace { + +bool fake_init(ProxySQL_PluginServices *, std::string &) { + return true; +} + +bool fake_start(std::string &) { + return true; +} + +bool fake_stop() { + return true; +} + +bool fake_status(std::string &) { + return true; +} + +const ProxySQL_PluginDescriptor fake_descriptor = { + 1, + "fake_plugin", + &fake_init, + &fake_start, + &fake_stop, + &fake_status, +}; + +} // namespace + +extern "C" const ProxySQL_PluginDescriptor *proxysql_plugin_descriptor_v1() { + return &fake_descriptor; +} diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index 30f678877..f5c881ebd 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -221,6 +221,7 @@ endif # =========================================================================== TEST_HELPERS_DIR := $(PROXYSQL_PATH)/test/tap/test_helpers +FAKE_PLUGIN_SO := $(TEST_HELPERS_DIR)/libproxysql_fake_plugin.so ODIR := obj TEST_HELPERS_OBJ := $(ODIR)/test_globals.o $(ODIR)/test_init.o $(ODIR)/tap.o @@ -236,11 +237,22 @@ $(ODIR)/tap.o: $(TAP_SRC) | $(ODIR) $(CXX) -c -o $@ $< $(OPT) $(IDIRS) -w $(ODIR)/test_globals.o: $(TEST_HELPERS_DIR)/test_globals.cpp | $(ODIR) - $(CXX) -c -o $@ $< $(OPT) $(IDIRS) -Wall + $(CXX) -c -o $@ $< $(OPT) $(IDIRS) -I$(PROXYSQL_PATH)/test -Wall $(ODIR)/test_init.o: $(TEST_HELPERS_DIR)/test_init.cpp | $(ODIR) $(CXX) -c -o $@ $< $(OPT) $(IDIRS) -Wall +$(FAKE_PLUGIN_SO): $(TEST_HELPERS_DIR)/fake_plugin.cpp | $(TEST_HELPERS_DIR) + $(CXX) -shared -fPIC -o $@ $< $(STDCPP) $(IDIRS) -ldl + +# Keep on-demand unit-test library rebuilds aligned with the default top-level +# feature set so incremental rebuilds do not mix incompatible objects. +$(LIBPROXYSQLAR): + $(MAKE) -C $(PROXYSQL_PATH)/lib libproxysql.a \ + PROXYSQLCLICKHOUSE=1 PROXYSQLGENAI=$(PROXYSQLGENAI) \ + PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) \ + PROXYSQL31=$(PROXYSQL31) CC=$(CC) CXX=$(CXX) + # =========================================================================== # Unit test targets @@ -288,7 +300,8 @@ UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \ gtid_utils_unit-t \ genai_mysql_catalog_unit-t \ admin_disk_upgrade_unit-t \ - glovars_unit-t + glovars_unit-t \ + plugin_manager_unit-t .PHONY: all all: $(UNIT_TESTS) @@ -312,6 +325,11 @@ ezoption_parser_unit-t: ezoption_parser_unit-t.cpp $(ODIR)/tap.o $(ODIR)/tap_noi -I$(TAP_IDIR) -I$(PROXYSQL_PATH)/include \ $(STDCPP) -O0 -ggdb $(WGCOV) $(LWGCOV) -lpthread -o $@ +plugin_manager_unit-t: plugin_manager_unit-t.cpp $(FAKE_PLUGIN_SO) $(ODIR)/tap.o $(ODIR)/test_globals.o $(ODIR)/test_init.o $(LIBPROXYSQLAR) + $(CXX) $< $(ODIR)/tap.o $(ODIR)/test_globals.o $(ODIR)/test_init.o \ + $(IDIRS) $(LDIRS) $(OPT) $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) \ + $(MYLIBS) -ldl $(ALLOW_MULTI_DEF) -o $@ + # Pattern rule: all unit tests use the same compile + link flags. # Each test binary is built from its .cpp source, linked against # the test harness objects and libproxysql.a with all dependencies. diff --git a/test/tap/tests/unit/plugin_manager_unit-t.cpp b/test/tap/tests/unit/plugin_manager_unit-t.cpp new file mode 100644 index 000000000..e2a9a4577 --- /dev/null +++ b/test/tap/tests/unit/plugin_manager_unit-t.cpp @@ -0,0 +1,24 @@ +#include "tap.h" +#include "ProxySQL_PluginManager.h" + +#include + +static void test_loader_round_trip() { + ProxySQL_PluginManager mgr; + std::string err; + + ok(mgr.load("../../test_helpers/libproxysql_fake_plugin.so", err), + "load fake plugin succeeds"); + ok(mgr.size() == 1, "exactly one plugin is loaded"); + ok(mgr.init_all(err), "init_all succeeds"); + ok(mgr.start_all(err), "start_all succeeds"); + ok(mgr.stop_all(), "stop_all succeeds"); +} + +int main() { + plan(5); + + test_loader_round_trip(); + + return exit_status(); +}