Phase 1b of the pluggable solver system. Converts KCSolve from a header-only INTERFACE target to a SHARED library and implements the SolverRegistry with dynamic plugin discovery. Changes: - Add KCSolveGlobal.h export macro header (KCSolveExport) - Move SolverRegistry method bodies from header to SolverRegistry.cpp - Implement scan() with dlopen/LoadLibrary plugin loading - Add scan_default_paths() for KCSOLVE_PLUGIN_PATH + system paths - Plugin entry points: kcsolve_api_version() + kcsolve_create() - API version checking (major version compatibility) - Convert CMakeLists.txt from INTERFACE to SHARED library - Link FreeCADBase (PRIVATE) for Console logging - Link dl on POSIX for dynamic loading - Fix -Wmissing-field-initializers warnings in IKCSolver.h defaults The registry discovers plugins by scanning directories for shared libraries that export the kcsolve C entry points. Plugins are validated for API version compatibility before registration. Manual registration via register_solver() remains available for built-in solvers (e.g. OndselAdapter in Phase 1c).
347 lines
10 KiB
C++
347 lines
10 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
/****************************************************************************
|
|
* *
|
|
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
|
* *
|
|
* This file is part of FreeCAD. *
|
|
* *
|
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
|
* under the terms of the GNU Lesser General Public License as *
|
|
* published by the Free Software Foundation, either version 2.1 of the *
|
|
* License, or (at your option) any later version. *
|
|
* *
|
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
|
* Lesser General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Lesser General Public *
|
|
* License along with FreeCAD. If not, see *
|
|
* <https://www.gnu.org/licenses/>. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "SolverRegistry.h"
|
|
|
|
#include <Base/Console.h>
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <sstream>
|
|
|
|
#ifdef _WIN32
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# include <windows.h>
|
|
#else
|
|
# include <dlfcn.h>
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace
|
|
{
|
|
|
|
// Platform extension for shared libraries.
|
|
#ifdef _WIN32
|
|
constexpr const char* PLUGIN_EXT = ".dll";
|
|
constexpr char PATH_SEP = ';';
|
|
#elif defined(__APPLE__)
|
|
constexpr const char* PLUGIN_EXT = ".dylib";
|
|
constexpr char PATH_SEP = ':';
|
|
#else
|
|
constexpr const char* PLUGIN_EXT = ".so";
|
|
constexpr char PATH_SEP = ':';
|
|
#endif
|
|
|
|
// Dynamic library loading wrappers.
|
|
void* open_library(const char* path)
|
|
{
|
|
#ifdef _WIN32
|
|
return static_cast<void*>(LoadLibraryA(path));
|
|
#else
|
|
return dlopen(path, RTLD_LAZY);
|
|
#endif
|
|
}
|
|
|
|
void* get_symbol(void* handle, const char* symbol)
|
|
{
|
|
#ifdef _WIN32
|
|
return reinterpret_cast<void*>(
|
|
GetProcAddress(static_cast<HMODULE>(handle), symbol));
|
|
#else
|
|
return dlsym(handle, symbol);
|
|
#endif
|
|
}
|
|
|
|
std::string load_error()
|
|
{
|
|
#ifdef _WIN32
|
|
DWORD err = GetLastError();
|
|
char* msg = nullptr;
|
|
FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
|
|
nullptr, err, 0, reinterpret_cast<char*>(&msg), 0, nullptr);
|
|
std::string result = msg ? msg : "unknown error";
|
|
LocalFree(msg);
|
|
return result;
|
|
#else
|
|
const char* err = dlerror();
|
|
return err ? err : "unknown error";
|
|
#endif
|
|
}
|
|
|
|
/// Parse major version from a version string like "1.0" or "2.1.3".
|
|
/// Returns -1 on failure.
|
|
int parse_major_version(const char* version_str)
|
|
{
|
|
if (!version_str) {
|
|
return -1;
|
|
}
|
|
char* end = nullptr;
|
|
long major = std::strtol(version_str, &end, 10);
|
|
if (end == version_str || major < 0) {
|
|
return -1;
|
|
}
|
|
return static_cast<int>(major);
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
|
|
namespace KCSolve
|
|
{
|
|
|
|
// Plugin C entry point types.
|
|
using ApiVersionFn = const char* (*)();
|
|
using CreateFn = IKCSolver* (*)();
|
|
|
|
|
|
// ── Singleton ──────────────────────────────────────────────────────
|
|
|
|
SolverRegistry& SolverRegistry::instance()
|
|
{
|
|
static SolverRegistry reg;
|
|
return reg;
|
|
}
|
|
|
|
SolverRegistry::SolverRegistry() = default;
|
|
|
|
SolverRegistry::~SolverRegistry()
|
|
{
|
|
for (void* handle : handles_) {
|
|
close_handle(handle);
|
|
}
|
|
}
|
|
|
|
void SolverRegistry::close_handle(void* handle)
|
|
{
|
|
if (!handle) {
|
|
return;
|
|
}
|
|
#ifdef _WIN32
|
|
FreeLibrary(static_cast<HMODULE>(handle));
|
|
#else
|
|
dlclose(handle);
|
|
#endif
|
|
}
|
|
|
|
|
|
// ── Registration ───────────────────────────────────────────────────
|
|
|
|
bool SolverRegistry::register_solver(const std::string& name, CreateSolverFn factory)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
auto [it, inserted] = factories_.emplace(name, std::move(factory));
|
|
if (!inserted) {
|
|
Base::Console().warning("KCSolve: solver '%s' already registered, skipping\n",
|
|
name.c_str());
|
|
return false;
|
|
}
|
|
if (default_name_.empty()) {
|
|
default_name_ = name;
|
|
}
|
|
Base::Console().log("KCSolve: registered solver '%s'\n", name.c_str());
|
|
return true;
|
|
}
|
|
|
|
|
|
// ── Lookup ─────────────────────────────────────────────────────────
|
|
|
|
std::unique_ptr<IKCSolver> SolverRegistry::get(const std::string& name) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
const std::string& key = name.empty() ? default_name_ : name;
|
|
if (key.empty()) {
|
|
return nullptr;
|
|
}
|
|
auto it = factories_.find(key);
|
|
if (it == factories_.end()) {
|
|
return nullptr;
|
|
}
|
|
return it->second();
|
|
}
|
|
|
|
std::vector<std::string> SolverRegistry::available() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
std::vector<std::string> names;
|
|
names.reserve(factories_.size());
|
|
for (const auto& [name, _] : factories_) {
|
|
names.push_back(name);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
std::vector<BaseJointKind> SolverRegistry::joints_for(const std::string& name) const
|
|
{
|
|
auto solver = get(name);
|
|
if (!solver) {
|
|
return {};
|
|
}
|
|
return solver->supported_joints();
|
|
}
|
|
|
|
bool SolverRegistry::set_default(const std::string& name)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (factories_.find(name) == factories_.end()) {
|
|
return false;
|
|
}
|
|
default_name_ = name;
|
|
return true;
|
|
}
|
|
|
|
std::string SolverRegistry::get_default() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return default_name_;
|
|
}
|
|
|
|
|
|
// ── Plugin scanning ────────────────────────────────────────────────
|
|
|
|
void SolverRegistry::scan(const std::string& directory)
|
|
{
|
|
std::error_code ec;
|
|
if (!fs::is_directory(directory, ec)) {
|
|
// Non-existent directories are not an error — just skip.
|
|
return;
|
|
}
|
|
|
|
Base::Console().log("KCSolve: scanning '%s' for plugins\n", directory.c_str());
|
|
|
|
for (const auto& entry : fs::directory_iterator(directory, ec)) {
|
|
if (ec) {
|
|
Base::Console().warning("KCSolve: error iterating '%s': %s\n",
|
|
directory.c_str(), ec.message().c_str());
|
|
break;
|
|
}
|
|
|
|
if (!entry.is_regular_file(ec)) {
|
|
continue;
|
|
}
|
|
|
|
const auto& path = entry.path();
|
|
if (path.extension() != PLUGIN_EXT) {
|
|
continue;
|
|
}
|
|
|
|
const std::string path_str = path.string();
|
|
|
|
// Load the shared library.
|
|
void* handle = open_library(path_str.c_str());
|
|
if (!handle) {
|
|
Base::Console().warning("KCSolve: failed to load '%s': %s\n",
|
|
path_str.c_str(), load_error().c_str());
|
|
continue;
|
|
}
|
|
|
|
// Check API version.
|
|
auto version_fn = reinterpret_cast<ApiVersionFn>(
|
|
get_symbol(handle, "kcsolve_api_version"));
|
|
if (!version_fn) {
|
|
// Not a KCSolve plugin — silently skip.
|
|
close_handle(handle);
|
|
continue;
|
|
}
|
|
|
|
const char* version_str = version_fn();
|
|
int major = parse_major_version(version_str);
|
|
if (major != API_VERSION_MAJOR) {
|
|
Base::Console().warning(
|
|
"KCSolve: plugin '%s' has incompatible API version '%s' "
|
|
"(expected major %d)\n",
|
|
path_str.c_str(),
|
|
version_str ? version_str : "(null)",
|
|
API_VERSION_MAJOR);
|
|
close_handle(handle);
|
|
continue;
|
|
}
|
|
|
|
// Get the factory symbol.
|
|
auto create_fn = reinterpret_cast<CreateFn>(
|
|
get_symbol(handle, "kcsolve_create"));
|
|
if (!create_fn) {
|
|
Base::Console().warning(
|
|
"KCSolve: plugin '%s' missing kcsolve_create() symbol\n",
|
|
path_str.c_str());
|
|
close_handle(handle);
|
|
continue;
|
|
}
|
|
|
|
// Create a temporary instance to get the solver name.
|
|
std::unique_ptr<IKCSolver> probe(create_fn());
|
|
if (!probe) {
|
|
Base::Console().warning(
|
|
"KCSolve: plugin '%s' kcsolve_create() returned null\n",
|
|
path_str.c_str());
|
|
close_handle(handle);
|
|
continue;
|
|
}
|
|
|
|
std::string solver_name = probe->name();
|
|
probe.reset();
|
|
|
|
// Wrap the C function pointer in a factory lambda.
|
|
CreateSolverFn factory = [create_fn]() -> std::unique_ptr<IKCSolver> {
|
|
return std::unique_ptr<IKCSolver>(create_fn());
|
|
};
|
|
|
|
if (register_solver(solver_name, std::move(factory))) {
|
|
handles_.push_back(handle);
|
|
Base::Console().log("KCSolve: loaded plugin '%s' from '%s'\n",
|
|
solver_name.c_str(), path_str.c_str());
|
|
}
|
|
else {
|
|
// Duplicate name — close the handle.
|
|
close_handle(handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SolverRegistry::scan_default_paths()
|
|
{
|
|
// 1. KCSOLVE_PLUGIN_PATH environment variable.
|
|
const char* env_path = std::getenv("KCSOLVE_PLUGIN_PATH");
|
|
if (env_path && env_path[0] != '\0') {
|
|
std::istringstream stream(env_path);
|
|
std::string dir;
|
|
while (std::getline(stream, dir, PATH_SEP)) {
|
|
if (!dir.empty()) {
|
|
scan(dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. System install path: <install_prefix>/lib/kcsolve/
|
|
// Derive from the executable location or use a compile-time path.
|
|
// For now, use a path relative to the FreeCAD lib directory.
|
|
std::error_code ec;
|
|
fs::path system_dir = fs::path(CMAKE_INSTALL_PREFIX) / "lib" / "kcsolve";
|
|
if (fs::is_directory(system_dir, ec)) {
|
|
scan(system_dir.string());
|
|
}
|
|
}
|
|
|
|
} // namespace KCSolve
|