// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * * * Copyright (c) 2025 Kindred Systems * * * * 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 * * . * * * ***************************************************************************/ #include "SolverRegistry.h" #include #include #include #include #include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # include #else # include #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(LoadLibraryA(path)); #else return dlopen(path, RTLD_LAZY); #endif } void* get_symbol(void* handle, const char* symbol) { #ifdef _WIN32 return reinterpret_cast( GetProcAddress(static_cast(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(&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(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(handle)); #else dlclose(handle); #endif } // ── Registration ─────────────────────────────────────────────────── bool SolverRegistry::register_solver(const std::string& name, CreateSolverFn factory) { std::lock_guard 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 SolverRegistry::get(const std::string& name) const { std::lock_guard 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 SolverRegistry::available() const { std::lock_guard lock(mutex_); std::vector names; names.reserve(factories_.size()); for (const auto& [name, _] : factories_) { names.push_back(name); } return names; } std::vector 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 lock(mutex_); if (factories_.find(name) == factories_.end()) { return false; } default_name_ = name; return true; } std::string SolverRegistry::get_default() const { std::lock_guard 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( 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( 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 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 { return std::unique_ptr(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: /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