feat: add OpenAI-compatible AI provider plugin with SSE streaming support

- Implemented the OpenAI-compatible AI provider plugin, including configuration, chat, and chat_stream functionalities.
- Added support for SSE streaming and tool calls.
- Integrated Boost.JSON for JSON handling.
- Created CMake configuration for the plugin.
- Added error handling and logging throughout the plugin.
This commit is contained in:
2026-05-31 05:37:04 +08:00
parent f6cb51b40a
commit ba7382db2a
61 changed files with 163 additions and 147 deletions

View File

@@ -0,0 +1,7 @@
/* @file boost_json.cpp
* @brief Boost.JSON header-only library compilation unit (single TU inclusion).
* Boost.JSON 仅头文件库的编译单元(单翻译单元包含)。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include <boost/json/src.hpp>

View File

@@ -0,0 +1,75 @@
/* @file config_store.cpp
* @brief ConfigStore implementation: TOML parsing, thread-safe get/set with thread-local safety.
* ConfigStore 实现TOML 解析、线程安全的 get/set基于 thread-local 安全机制)。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include "config_store.hpp"
#include "../../plugins_base/config/include/toml_parse.h"
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
namespace dstalk {
// 在互斥锁下加载并解析 TOML 文件到键值存储 / Load and parse a TOML file into the key-value store under mutex.
int ConfigStore::load_file(const char* path)
{
if (!path) return -1;
std::ifstream file(path);
if (!file.is_open()) return -1;
std::stringstream ss;
ss << file.rdbuf();
std::string data = ss.str();
// W12.2: 使用共享 TOML 解析器(从 config_plugin.cpp 去重) / Use shared TOML parser (de-duplicated from config_plugin.cpp)
toml::parse(data, [this](const std::string& key, const std::string& value) {
std::lock_guard<std::mutex> lock(mutex_);
data_[key] = value;
});
return 0;
}
// 检索配置值,返回线程本地副本以避免 c_str() 悬空 / Retrieve config value, returning a thread-local copy to avoid dangling c_str().
const char* ConfigStore::get(const char* key) const
{
if (!key) return nullptr;
std::lock_guard<std::mutex> lock(mutex_);
auto it = data_.find(key);
if (it == data_.end()) return nullptr;
// W12.2: 在释放锁之前复制到线程本地缓冲区 /
// Copy to thread-local buffer before releasing lock.
// 防止当并发 set() 触发 std::string 重新分配时 c_str() 悬空 /
// Prevents c_str() dangling when concurrent set() on the same key
// triggers std::string reallocation (W11.2 audit Finding 3).
thread_local std::string tls_cached;
tls_cached = it->second;
return tls_cached.c_str();
}
// 以 std::string 值类型检索配置(安全的值副本)/ Retrieve config value as an owned std::string (safe by-value copy).
std::string ConfigStore::get_copy(const char* key) const
{
if (!key) return {};
std::lock_guard<std::mutex> lock(mutex_);
auto it = data_.find(key);
if (it == data_.end()) return {};
return it->second; // 在锁下复制构造,始终安全 / copy-constructed under lock, always safe
}
// 在锁下设置配置键值对 / Set a config key-value pair under lock.
int ConfigStore::set(const char* key, const char* value)
{
if (!key || !value) return -1;
std::lock_guard<std::mutex> lock(mutex_);
data_[key] = value;
return 0;
}
} // namespace dstalk

View File

@@ -0,0 +1,47 @@
/* @file config_store.hpp
* @brief Thread-safe key-value configuration store with TOML file loading.
* 线程安全键值配置存储,支持 TOML 文件加载。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#pragma once
#include <mutex>
#include <string>
#include <unordered_map>
namespace dstalk {
// 线程安全的键值存储,支持 TOML 配置文件 / Thread-safe key-value store backed by TOML config files.
// 通过 mutex_ 支持并发读取get() 返回线程本地缓冲区 / Supports concurrent reads via mutex_ and returns thread-local buffers from get().
class ConfigStore {
public:
ConfigStore() = default;
~ConfigStore() = default;
// 从 TOML 文件加载键值对 / Load key-value pairs from a TOML file.
// 成功返回 0文件未找到或路径为空返回 -1 / Returns 0 on success, -1 if file not found or path is null.
int load_file(const char* path);
// 获取配置值(返回内部指针,线程安全)/ Get config value (returns internal pointer, thread-safe).
// W12.2: 返回的指针现在由线程局部副本支持,对其他线程对同一键的并发 set() 安全 /
// Returned pointer is now backed by a thread-local copy;
// safe against concurrent set() on the same key from other threads.
// 调用者仍应立即使用 — 同一线程上的下一次 get() 将覆盖缓冲区 /
// Caller should still consume immediately — next get() on same
// thread will overwrite the buffer.
const char* get(const char* key) const;
// 获取配置项的安全值副本(无悬空风险)/ Get a safe by-value copy of a config entry (no dangling risk).
// 如果键未找到,返回空字符串 / Returns empty string if key not found.
std::string get_copy(const char* key) const;
// 设置配置值 / Set config value. 成功返回 0参数为空返回 -1 / Returns 0 on success, -1 on null arguments.
int set(const char* key, const char* value);
private:
mutable std::mutex mutex_; // 保护所有 data_ 访问 / Protects all data_ access
std::unordered_map<std::string, std::string> data_; // 配置键值存储 / Config key-value store
};
} // namespace dstalk

View File

@@ -0,0 +1,49 @@
/* @file event_bus.cpp
* @brief EventBus implementation: subscribe, unsubscribe, emit with reader-writer locking.
* EventBus 实现:基于读写锁的 subscribe、unsubscribe、emit。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include "event_bus.hpp"
#include <algorithm>
namespace dstalk {
// 为给定事件类型注册处理器,返回订阅 ID / Register a handler for the given event type, returning a subscription id.
int EventBus::subscribe(int event_type, EventHandler handler)
{
std::unique_lock<std::shared_mutex> lock(mutex_);
int id = next_id_++;
subscriptions_.push_back({id, event_type, std::move(handler)});
return id;
}
// 通过 ID 移除订阅(如果 ID 未找到则无操作)/ Remove a subscription by id (no-op if id not found).
void EventBus::unsubscribe(int subscription_id)
{
std::unique_lock<std::shared_mutex> lock(mutex_);
subscriptions_.erase(
std::remove_if(subscriptions_.begin(), subscriptions_.end(),
[subscription_id](const Subscription& s) {
return s.id == subscription_id;
}),
subscriptions_.end());
}
// 在共享锁下将事件分发给所有匹配的订阅者 / Dispatch an event to all matching subscribers under a shared lock.
// 返回被调用的处理器数量 / Returns the count of handlers invoked.
int EventBus::emit(int event_type, const void* data)
{
std::shared_lock<std::shared_mutex> lock(mutex_);
int count = 0;
for (const auto& sub : subscriptions_) {
if (sub.event_type == event_type) {
sub.handler(event_type, data);
count++;
}
}
return count;
}
} // namespace dstalk

View File

@@ -0,0 +1,50 @@
/* @file event_bus.hpp
* @brief Publish-subscribe event bus with shared_mutex for concurrent read access.
* 发布-订阅事件总线,使用 shared_mutex 支持并发读访问。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#pragma once
#include <functional>
#include <mutex>
#include <shared_mutex>
#include <unordered_map>
#include <vector>
namespace dstalk {
using EventHandler = std::function<void(int event_type, const void* data)>;
// 轻量级发布-订阅事件总线 / Lightweight pub-sub event bus.
// 读取者使用 shared_lockemit因此多个处理器可以并发分发
// 写入者使用 unique_locksubscribe / unsubscribe
// Readers use shared_lock (emit) so multiple handlers can be dispatched
// concurrently; writers use unique_lock (subscribe / unsubscribe).
class EventBus {
public:
EventBus() = default;
~EventBus() = default;
// 订阅事件返回订阅ID / Subscribe to an event, returning a subscription id
int subscribe(int event_type, EventHandler handler);
// 取消订阅 / Unsubscribe by subscription id
void unsubscribe(int subscription_id);
// 发布事件 / Emit an event to all matching subscribers
int emit(int event_type, const void* data);
private:
struct Subscription {
int id;
int event_type;
EventHandler handler;
};
mutable std::shared_mutex mutex_; // 读写锁emit 用 sharedsubscribe/unsubscribe 用 unique / RW lock: shared for emit, unique for subscribe/unsubscribe
std::vector<Subscription> subscriptions_; // emit 时线性扫描;对少量订阅者足够 / Linear scan on emit; ok for small subscriber counts
int next_id_ = 1; // 单调递增订阅 ID 计数器 / Monotonic subscription id counter
};
} // namespace dstalk

460
dstalk_core/src/host.cpp Normal file
View File

@@ -0,0 +1,460 @@
/*
* @file host.cpp
* @brief Core host orchestrator: global singletons, dstalk_host_api_t instantiation, public C API, LSP delegation.
* 核心主机协调器全局单例、dstalk_host_api_t 实例化、公共 C API、LSP 委托。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include "dstalk/dstalk_host.h"
#include "config_store.hpp"
#include "event_bus.hpp"
#include "service_registry.hpp"
#include "plugin_loader.hpp"
#include <atomic>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <mutex>
namespace fs = std::filesystem;
// ============================================================
// 全局主机上下文 / Global host context
// ============================================================
namespace {
std::mutex g_init_mutex;
bool g_initialized = false;
dstalk::ConfigStore* g_config = nullptr;
dstalk::EventBus* g_event_bus = nullptr;
dstalk::ServiceRegistry* g_service_registry = nullptr;
dstalk::PluginLoader* g_plugin_loader = nullptr;
static std::atomic<dstalk_diag_cb> g_diag_callback{nullptr};
// ---- 内部辅助 / Internal helpers ----
// 复制 C 字符串(用 malloc 分配,调用者必须用 api_free/free 释放)
// Duplicate a C string allocated with malloc (caller must free via api_free/free).
char* host_strdup(const char* s) {
if (!s) return nullptr;
size_t len = strlen(s);
char* copy = (char*)malloc(len + 1);
if (copy) memcpy(copy, s, len + 1);
return copy;
}
// 核心日志实现:格式化消息,写入 stderr并转发到诊断回调如果已设置
// Core logging implementation: formats message, writes to stderr, and forwards to diagnostic callback if set.
void host_log_impl(int level, const char* fmt, va_list args) {
const char* prefix = "";
switch (level) {
case DSTALK_LOG_DEBUG: prefix = "[DEBUG] "; break;
case DSTALK_LOG_INFO: prefix = "[INFO] "; break;
case DSTALK_LOG_WARN: prefix = "[WARN] "; break;
case DSTALK_LOG_ERROR: prefix = "[ERROR] "; break;
}
fprintf(stderr, "%s", prefix);
va_list args_copy;
va_copy(args_copy, args);
vfprintf(stderr, fmt, args);
fprintf(stderr, "\n");
// 转发到诊断回调 / Forward to diagnostic callback
auto cb = g_diag_callback.load(std::memory_order_acquire);
if (cb) {
char buf[1024];
vsnprintf(buf, sizeof(buf), fmt, args_copy);
cb(level, nullptr, 0, nullptr, buf);
}
va_end(args_copy);
}
// host_log_impl 的 printf 风格便捷包装。
// Convenience wrapper around host_log_impl for printf-style calls.
void host_log(int level, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
host_log_impl(level, fmt, args);
va_end(args);
}
// ---- Host API 表回调 / Host API table callbacks ----
// 将服务 vtable 按名称和版本注册到全局注册表。
// Register a service vtable with the given name and version into the global registry.
int api_register_service(const char* name, int version, void* vtable) {
return g_service_registry ? g_service_registry->register_service(name, version, vtable) : -1;
}
// 按名称和最低版本从全局注册表查询服务 vtable。
// Query a service vtable by name and minimum version from the global registry.
void* api_query_service(const char* name, int min_version) {
return g_service_registry ? g_service_registry->query_service(name, min_version) : nullptr;
}
// 通过全局事件总线订阅指定事件类型的处理函数。
// Subscribe a handler to a given event type via the global event bus.
int api_event_subscribe(int event_type, dstalk_event_handler_fn handler, void* userdata) {
if (!g_event_bus || !handler) return -1;
return g_event_bus->subscribe(event_type,
[handler, userdata](int type, const void* data) {
handler(type, data, userdata);
});
}
// 通过全局事件总路线程安全地向所有已注册处理函数发送事件。
// Emit an event to all registered handlers via the global event bus.
int api_event_emit(int event_type, const void* data) {
return g_event_bus ? g_event_bus->emit(event_type, data) : -1;
}
// 通过订阅 ID 取消注册之前的事件处理函数。
// Unsubscribe a previously registered event handler by subscription ID.
void api_event_unsubscribe(int sub_id) {
if (g_event_bus) g_event_bus->unsubscribe(sub_id);
}
// 从全局配置存储中按键名读取配置值。
// Read a config value by key from the global config store.
const char* api_config_get(const char* key) {
return g_config ? g_config->get(key) : nullptr;
}
// 在全局配置存储中设置配置键值对。
// Set a config key/value pair in the global config store.
int api_config_set(const char* key, const char* value) {
return g_config ? g_config->set(key, value) : -1;
}
// 主机端日志函数host_log_impl 的 varargs 包装)。
// Host-facing log function (varargs wrapper around host_log_impl).
void api_log(int level, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
host_log_impl(level, fmt, args);
va_end(args);
}
// 内存分配包装 / Memory allocation wrapper (malloc).
void* api_alloc(size_t size) { return malloc(size); }
// 内存释放包装 / Memory free wrapper (free).
void api_free(void* ptr) { free(ptr); }
// 字符串复制包装 / String duplication wrapper (host_strdup).
char* api_strdup(const char* s) { return host_strdup(s); }
// 传递给每个插件 on_init 的完整主机 API vtable。
// The complete host API vtable passed to every plugin's on_init.
dstalk_host_api_t g_host_api = {
api_register_service,
api_query_service,
api_event_subscribe,
api_event_emit,
api_event_unsubscribe,
api_config_get,
api_config_set,
api_log,
api_alloc,
api_free,
api_strdup
};
// ---- 插件目录扫描 / Plugin directory scanning ----
// 扫描目录中的插件 DLL 并通过 PluginLoader 加载。
// 返回加载的插件数量,出错返回 -1。
// Scan a directory for plugin DLLs and load them via PluginLoader.
// Returns the number of plugins loaded, or -1 on error.
int load_plugins_from_directory(const char* plugin_dir) {
if (!plugin_dir) return -1;
try {
fs::path dir(plugin_dir);
if (!fs::exists(dir) || !fs::is_directory(dir)) return -1;
int loaded = 0;
for (const auto& entry : fs::directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
std::string ext = entry.path().extension().string();
#ifdef _WIN32
if (ext != ".dll") continue;
#else
if (ext != ".so" && ext != ".dylib") continue;
#endif
int id = g_plugin_loader->load_plugin(entry.path().string().c_str());
if (id >= 0) {
loaded++;
host_log(DSTALK_LOG_INFO, "Loaded plugin: %s",
entry.path().filename().string().c_str());
}
}
return loaded;
} catch (const std::exception& e) {
host_log(DSTALK_LOG_ERROR, "Failed to scan plugin directory: %s", e.what());
return -1;
}
}
}
// ============================================================
// 公共 API / Public API
// ============================================================
// 初始化 dstalk 主机:创建单例、加载配置、扫描插件、初始化所有插件。
// Initialize the dstalk host: create singletons, load config, scan plugins, initialize all plugins.
DSTALK_API int dstalk_init(const char* config_path)
{
std::lock_guard<std::mutex> lock(g_init_mutex);
if (g_initialized) return -1;
try {
g_config = new dstalk::ConfigStore();
g_event_bus = new dstalk::EventBus();
g_service_registry = new dstalk::ServiceRegistry();
g_plugin_loader = new dstalk::PluginLoader();
// 加载配置 / Load config
if (config_path && config_path[0]) {
if (g_config->load_file(config_path) != 0) {
host_log(DSTALK_LOG_WARN, "Failed to load config: %s", config_path);
}
}
// 扫描插件目录 / Scan plugin directories
// 优先扫描三级插件目录,回退到单一 plugins/ 目录(构建输出)
// Prefer three-tier plugin dirs, fall back to single plugins/ dir (build output)
const char* dirs[] = {
"plugins_base", "plugins_middle", "plugins_upper",
"../plugins_base", "../plugins_middle", "../plugins_upper",
"plugins", "../plugins",
nullptr
};
int loaded = 0;
for (int i = 0; dirs[i]; ++i) {
int n = load_plugins_from_directory(dirs[i]);
if (n > 0) loaded += n;
}
if (loaded <= 0) {
host_log(DSTALK_LOG_WARN, "No plugins found in any plugin directory");
}
// 初始化所有插件 / Initialize all plugins
if (g_plugin_loader->initialize_all(&g_host_api) != 0) {
host_log(DSTALK_LOG_WARN, "Some plugins failed to initialize");
}
g_initialized = true;
host_log(DSTALK_LOG_INFO, "dstalk host initialized");
return 0;
} catch (const std::exception& e) {
host_log(DSTALK_LOG_ERROR, "Init failed: %s", e.what());
delete g_plugin_loader; g_plugin_loader = nullptr;
delete g_service_registry; g_service_registry = nullptr;
delete g_event_bus; g_event_bus = nullptr;
delete g_config; g_config = nullptr;
return -1;
}
}
// 关闭 dstalk 主机:关闭插件、销毁单例、释放资源。
// Shutdown the dstalk host: shutdown plugins, destroy singletons, release resources.
DSTALK_API void dstalk_shutdown(void)
{
std::lock_guard<std::mutex> lock(g_init_mutex);
if (!g_initialized) return;
host_log(DSTALK_LOG_INFO, "dstalk shutting down...");
if (g_plugin_loader) {
g_plugin_loader->shutdown_all();
delete g_plugin_loader;
g_plugin_loader = nullptr;
}
delete g_service_registry; g_service_registry = nullptr;
delete g_event_bus; g_event_bus = nullptr;
delete g_config; g_config = nullptr;
g_initialized = false;
}
// 从给定路径加载单个插件 DLL 并初始化。返回插件 ID失败返回 -1。
// Load a single plugin DLL from the given path and initialize it. Returns plugin ID or -1.
DSTALK_API int dstalk_plugin_load(const char* path)
{
if (!g_initialized || !g_plugin_loader) return -1;
int id = g_plugin_loader->load_plugin(path);
if (id >= 0) {
g_plugin_loader->initialize_pending(&g_host_api);
}
return id;
}
// 按 ID 卸载插件:调用 on_shutdown卸载 DLL从注册表中移除。成功返回 0。
// Unload a plugin by ID: call on_shutdown, unload DLL, remove from registry. Returns 0 on success.
DSTALK_API int dstalk_plugin_unload(int plugin_id)
{
if (!g_initialized || !g_plugin_loader) return -1;
return g_plugin_loader->unload_plugin(plugin_id);
}
// 以 JSON 字符串列出所有已加载插件。调用者必须用 dstalk_free 释放 *output_json。
// List all loaded plugins as a JSON string. Caller must free *output_json with dstalk_free.
DSTALK_API int dstalk_plugin_list(char** output_json)
{
if (!g_initialized || !g_plugin_loader || !output_json) return -1;
*output_json = host_strdup(g_plugin_loader->list_plugins().c_str());
return *output_json ? 0 : -1;
}
// 按名称和最低版本从全局服务注册表查询服务 vtable。
// Query a service vtable by name and minimum version from the global service registry.
DSTALK_API void* dstalk_service_query(const char* service_name, int min_version)
{
if (!g_initialized || !g_service_registry) return nullptr;
return g_service_registry->query_service(service_name, min_version);
}
// 订阅回调到事件类型。返回订阅 ID失败返回 -1。
// Subscribe a callback to an event type. Returns a subscription ID or -1.
DSTALK_API int dstalk_event_subscribe(int event_type, dstalk_event_handler_fn handler, void* userdata)
{
if (!g_initialized || !g_event_bus || !handler) return -1;
return g_event_bus->subscribe(event_type,
[handler, userdata](int type, const void* data) { handler(type, data, userdata); });
}
// 向订阅了该事件类型的所有处理函数发送事件。
// Emit an event to all handlers subscribed to the given event type.
DSTALK_API int dstalk_event_emit(int event_type, const void* data)
{
if (!g_initialized || !g_event_bus) return -1;
return g_event_bus->emit(event_type, data);
}
// 按订阅 ID 取消注册之前的事件处理函数。
// Unsubscribe a previously registered event handler by subscription ID.
DSTALK_API void dstalk_event_unsubscribe(int subscription_id)
{
if (!g_initialized || !g_event_bus) return;
g_event_bus->unsubscribe(subscription_id);
}
// 按键读取配置值。返回指向内部存储的指针(请勿释放)。
// Read a configuration value by key. Returns pointer to internal storage (do not free).
DSTALK_API const char* dstalk_config_get(const char* key)
{
if (!g_initialized || !g_config) return nullptr;
return g_config->get(key);
}
// 设置配置键值对。成功返回 0。
// Set a configuration key/value pair. Returns 0 on success.
DSTALK_API int dstalk_config_set(const char* key, const char* value)
{
if (!g_initialized || !g_config) return -1;
return g_config->set(key, value);
}
// 在给定级别记录消息printf 风格)。写入 stderr 并转发到诊断回调。
// Log a message at the given level (printf-style). Writes to stderr and forwards to diag callback.
DSTALK_API void dstalk_log(int level, const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
host_log_impl(level, fmt, args);
va_end(args);
}
// 通过 malloc 分配内存(为插件 ABI 一致性提供) / Allocate memory via malloc (provided for plugin ABI consistency).
DSTALK_API void* dstalk_alloc(size_t size) { return malloc(size); }
// 释放通过 dstalk_alloc 分配的内存(为插件 ABI 一致性提供) / Free memory allocated via dstalk_alloc (provided for plugin ABI consistency).
DSTALK_API void dstalk_free(void* ptr) { free(ptr); }
// 使用 dstalk_alloc 复制 C 字符串(调用者必须 dstalk_free / Duplicate a C string using dstalk_alloc (caller must dstalk_free).
DSTALK_API char* dstalk_strdup(const char* s) { return host_strdup(s); }
// 注册接收所有日志消息的诊断回调(传入 null 可取消设置)。
// Register a diagnostic callback that receives all log messages (may be null to unset).
DSTALK_API void dstalk_set_diag_callback(dstalk_diag_cb cb) {
g_diag_callback.store(cb, std::memory_order_release);
}
// ============================================================
// LSP 便捷函数 (委托给 "lsp" 服务插件) / LSP convenience functions (delegated to "lsp" service plugin)
// ============================================================
// 从全局服务注册表获取 "lsp" 服务 vtable不可用则返回 null。
// Retrieve the "lsp" service vtable from the global service registry, or null if unavailable.
static const dstalk_lsp_service_t* get_lsp_service() {
if (!g_initialized || !g_service_registry) return nullptr;
return static_cast<const dstalk_lsp_service_t*>(
g_service_registry->query_service("lsp", 1));
}
// 为给定的命令和语言启动语言服务器进程。
// Start a language server process for the given command and language.
DSTALK_API int dstalk_lsp_start(const char* server_cmd, const char* language)
{
auto* svc = get_lsp_service();
if (!svc || !svc->start) return -1;
return svc->start(server_cmd, language);
}
// 停止当前正在运行的语言服务器进程。
// Stop the currently running language server process.
DSTALK_API void dstalk_lsp_stop(void)
{
auto* svc = get_lsp_service();
if (svc && svc->stop) svc->stop();
}
// 在 LSP 服务器中打开文档以供分析didOpen 通知)。
// Open a document in the LSP server for analysis (didOpen notification).
DSTALK_API int dstalk_lsp_open(const char* uri, const char* content, const char* language_id)
{
auto* svc = get_lsp_service();
if (!svc || !svc->open_document) return -1;
return svc->open_document(uri, content, language_id);
}
// 在 LSP 服务器中关闭文档didClose 通知)。
// Close a document in the LSP server (didClose notification).
DSTALK_API int dstalk_lsp_close(const char* uri)
{
auto* svc = get_lsp_service();
if (!svc || !svc->close_document) return -1;
return svc->close_document(uri);
}
// 检索文档的当前诊断信息。调用者必须用 dstalk_free 释放 *output。
// Retrieve current diagnostics for a document. Caller must free *output with dstalk_free.
DSTALK_API int dstalk_lsp_diagnostics(const char* uri, char** output)
{
auto* svc = get_lsp_service();
if (!svc || !svc->get_diagnostics) return -1;
return svc->get_diagnostics(uri, output);
}
// 请求文档位置处的悬停信息。调用者必须用 dstalk_free 释放 *output。
// Request hover information at a document position. Caller must free *output with dstalk_free.
DSTALK_API int dstalk_lsp_hover(const char* uri, int line, int character, char** output)
{
auto* svc = get_lsp_service();
if (!svc || !svc->get_hover) return -1;
return svc->get_hover(uri, line, character, output);
}
// 请求文档位置处的补全项。调用者必须用 dstalk_free 释放 *output。
// Request completion items at a document position. Caller must free *output with dstalk_free.
DSTALK_API int dstalk_lsp_completion(const char* uri, int line, int character, char** output)
{
auto* svc = get_lsp_service();
if (!svc || !svc->get_completion) return -1;
return svc->get_completion(uri, line, character, output);
}

View File

@@ -0,0 +1,563 @@
/*
* @file plugin_loader.cpp
* @brief PluginLoader implementation: DLL load/unload, path validation, Kahn topological sort, lifecycle management.
* PluginLoader 实现DLL 加载/卸载、路径验证、Kahn 拓扑排序、生命周期管理。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include "plugin_loader.hpp"
#include <boost/json.hpp>
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include <algorithm>
#include <cctype>
#include <exception>
#include <filesystem>
#include <queue>
#include <stdexcept>
#include <unordered_set>
namespace dstalk {
namespace json = boost::json;
namespace fs = std::filesystem;
// 析构函数:调用 shutdown_all 释放所有插件并释放 DLL 句柄。
// Destructor: calls shutdown_all to release all plugins and free DLL handles.
PluginLoader::~PluginLoader()
{
shutdown_all();
}
// 加载插件 DLL验证路径扩展名、目录遍历、目录加载库
// 解析 dstalk_plugin_init验证 API 版本,解析依赖,分配 ID。
// Load a plugin DLL: validate path (extension, traversal, directory), load library,
// resolve dstalk_plugin_init, verify API version, parse dependencies, assign ID.
int PluginLoader::load_plugin(const char* path)
{
if (!path) return -1;
// === 路径验证 (F-18.3-3) / Path validation (F-18.3-3) ===
{
fs::path p = fs::absolute(fs::path(path)).lexically_normal();
// 扩展名检查(大小写不敏感) / Extension check (case-insensitive)
std::string ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
bool valid_ext = false;
#ifdef _WIN32
valid_ext = (ext == ".dll");
#elif defined(__APPLE__)
valid_ext = (ext == ".dylib" || ext == ".so");
#else
valid_ext = (ext == ".so");
#endif
if (!valid_ext) {
if (host_api_) {
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': invalid extension '%s', expected .dll/.so/.dylib",
path, ext.c_str());
}
return -1;
}
// 目录遍历检查 / Directory traversal check
bool has_dotdot = false;
bool in_plugins_dir = false;
for (const auto& comp : p) {
if (comp == "..") {
has_dotdot = true;
break;
}
std::string comp_str = comp.string();
if (comp_str == "plugins" ||
comp_str.substr(0, 8) == "plugins_") {
in_plugins_dir = true;
}
}
if (has_dotdot) {
if (host_api_) {
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': directory traversal rejected", path);
}
return -1;
}
// 目录约束:必须位于 'plugins' 或 'plugins_*' 目录下,或为纯文件名
// Directory constraint: must be under a 'plugins' or 'plugins_*' directory, or be a plain filename
if (!in_plugins_dir && p.has_parent_path()) {
if (host_api_) {
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': path not under a 'plugins' directory", path);
}
return -1;
}
}
// 加载DLL / Load DLL
#ifdef _WIN32
void* handle = LoadLibraryA(path);
#else
void* handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
#endif
if (!handle) {
if (host_api_) {
#ifdef _WIN32
DWORD err = GetLastError();
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': LoadLibraryA failed (error %lu)", path, (unsigned long)err);
#else
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': dlopen failed: %s", path, dlerror());
#endif
}
return -1;
}
// 获取入口函数 / Resolve entry function
#ifdef _WIN32
auto init_fn = (dstalk_plugin_init_fn)GetProcAddress(
(HMODULE)handle, "dstalk_plugin_init");
#else
auto init_fn = (dstalk_plugin_init_fn)dlsym(handle, "dstalk_plugin_init");
#endif
if (!init_fn) {
if (host_api_) {
#ifdef _WIN32
DWORD err = GetLastError();
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': GetProcAddress(dstalk_plugin_init) failed (error %lu)",
path, (unsigned long)err);
#else
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': dlsym(dstalk_plugin_init) failed: %s",
path, dlerror());
#endif
}
#ifdef _WIN32
FreeLibrary((HMODULE)handle);
#else
dlclose(handle);
#endif
return -1;
}
// 调用入口函数获取插件信息 / Call entry function to get plugin info
dstalk_plugin_info_t* info = nullptr;
try {
info = init_fn();
} catch (const std::exception& e) {
if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] %s: init_fn threw: %s", path, e.what());
} catch (...) {
if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] %s: init_fn threw unknown exception", path);
}
if (!info) {
if (host_api_) {
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': dstalk_plugin_init returned null", path);
}
#ifdef _WIN32
FreeLibrary((HMODULE)handle);
#else
dlclose(handle);
#endif
return -1;
}
// 检查API版本兼容性 / Check API version compatibility
if (info->api_version != DSTALK_API_VERSION) {
if (host_api_) {
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] '%s': API version mismatch (got %d, expected %d)",
path, info->api_version, DSTALK_API_VERSION);
}
#ifdef _WIN32
FreeLibrary((HMODULE)handle);
#else
dlclose(handle);
#endif
return -1;
}
// 创建插件信息 / Create plugin info
int id = next_id_++;
PluginInfo plugin;
plugin.id = id;
plugin.name = info->name ? info->name : "";
plugin.version = info->version ? info->version : "";
plugin.description = info->description ? info->description : "";
plugin.api_version = info->api_version;
plugin.handle = handle;
plugin.info = info;
plugin.initialized = false;
// 解析依赖 / Parse dependencies
for (int i = 0; i < DSTALK_MAX_DEPS && info->dependencies[i]; i++) {
plugin.dependencies.push_back(info->dependencies[i]);
}
plugins_[id] = std::move(plugin);
return id;
}
// 按 ID 卸载插件:若已初始化则调用 on_shutdown释放 DLL 句柄,从 map 中移除。
// Unload a plugin by ID: call on_shutdown if initialized, free the DLL handle, erase from map.
int PluginLoader::unload_plugin(int plugin_id)
{
auto it = plugins_.find(plugin_id);
if (it == plugins_.end()) return -1;
PluginInfo& plugin = it->second;
// 调用关闭回调 / Call shutdown callback
if (plugin.initialized && plugin.info->on_shutdown) {
try {
plugin.info->on_shutdown();
} catch (const std::exception& e) {
if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' on_shutdown threw: %s",
plugin.name.c_str(), e.what());
} catch (...) {
if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' on_shutdown threw unknown exception",
plugin.name.c_str());
}
}
// 卸载DLL / Unload DLL
#ifdef _WIN32
FreeLibrary((HMODULE)plugin.handle);
#else
dlclose(plugin.handle);
#endif
plugins_.erase(it);
return 0;
}
// 将所有已加载插件序列化为 JSON 数组字符串。
// Serialize all loaded plugins into a JSON array string.
std::string PluginLoader::list_plugins() const
{
json::array arr;
for (const auto& [id, plugin] : plugins_) {
json::object obj;
obj["id"] = id;
obj["name"] = plugin.name;
obj["version"] = plugin.version;
obj["description"] = plugin.description;
obj["api_version"] = plugin.api_version;
obj["initialized"] = plugin.initialized;
json::array deps;
for (const auto& dep : plugin.dependencies) {
deps.push_back(json::value(dep));
}
obj["dependencies"] = std::move(deps);
arr.push_back(std::move(obj));
}
return json::serialize(arr);
}
// 使用 Kahn 算法计算依赖顺序的插件 ID 列表。
// 若检测到循环依赖则抛出 std::runtime_error。
// Compute dependency-ordered plugin IDs using Kahn's algorithm.
// Throws std::runtime_error if a circular dependency is detected.
std::vector<int> PluginLoader::topological_sort() const
{
// 构建名称到ID的映射 / Build name-to-ID map
std::unordered_map<std::string, int> name_to_id;
for (const auto& [id, plugin] : plugins_) {
name_to_id[plugin.name] = id;
}
// 计算入度 / Calculate in-degrees
std::unordered_map<int, int> in_degree;
std::unordered_map<int, std::vector<int>> dependents;
for (const auto& [id, plugin] : plugins_) {
in_degree[id] = 0;
}
for (const auto& [id, plugin] : plugins_) {
for (const auto& dep_name : plugin.dependencies) {
auto it = name_to_id.find(dep_name);
if (it != name_to_id.end()) {
int dep_id = it->second;
dependents[dep_id].push_back(id);
in_degree[id]++;
}
}
}
// 拓扑排序Kahn算法 / Topological sort (Kahn's algorithm)
std::queue<int> queue;
for (const auto& [id, degree] : in_degree) {
if (degree == 0) {
queue.push(id);
}
}
std::vector<int> sorted;
while (!queue.empty()) {
int id = queue.front();
queue.pop();
sorted.push_back(id);
for (int dependent : dependents[id]) {
if (--in_degree[dependent] == 0) {
queue.push(dependent);
}
}
}
// 检查循环依赖 / Check for circular dependency
if (sorted.size() != plugins_.size()) {
throw std::runtime_error("Circular dependency detected");
}
return sorted;
}
// 验证依赖:检查缺失依赖和循环依赖。
// 成功返回 0发现错误返回 -1错误通过 host_api_ 记录)。
// Validate dependencies: checks for missing dependencies and circular dependencies.
// Returns 0 on success, -1 if any errors found (errors are logged via host_api_).
int PluginLoader::validate_dependencies() const
{
int error_count = 0;
// 构建名称到ID的映射 / Build name-to-ID map
std::unordered_map<std::string, int> name_to_id;
for (const auto& [id, plugin] : plugins_) {
name_to_id[plugin.name] = id;
}
// 检查1缺失依赖deps 引用的插件未加载) / Check 1: missing dependencies (deps reference plugins not loaded)
for (const auto& [id, plugin] : plugins_) {
for (const auto& dep_name : plugin.dependencies) {
if (name_to_id.find(dep_name) == name_to_id.end()) {
if (host_api_) {
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] Plugin '%s': dependency '%s' not found (plugin not loaded)",
plugin.name.c_str(), dep_name.c_str());
}
error_count++;
}
}
}
// 检查2循环依赖拓扑排序失败 / Check 2: circular dependency (topological sort fails)
try {
topological_sort();
} catch (const std::runtime_error&) {
if (host_api_) {
host_api_->log(DSTALK_LOG_ERROR,
"[plugin_loader] Circular dependency detected among loaded plugins");
}
error_count++;
}
return error_count > 0 ? -1 : 0;
}
// 按依赖顺序初始化所有未初始化的插件。
// 无效依赖或失败初始化会标记插件名,避免级联失败。
// 返回初始化失败的插件数量,严重错误返回 -1。
// Initialize all uninitialized plugins in dependency order.
// Invalid dependencies or failed inits mark the plugin name, avoiding cascading failures.
// Returns the number of plugins that failed to initialize, or -1 on critical error.
int PluginLoader::initialize_all(const dstalk_host_api_t* host_api)
{
if (!host_api) return -1;
host_api_ = host_api;
// 依赖合法性校验log 错误但不 crash继续初始化流程
// Validate dependencies (log errors but don't crash, continue initialization)
if (validate_dependencies() != 0) {
host_api->log(DSTALK_LOG_WARN,
"[plugin_loader] Dependency validation failed; initialization may be incomplete");
}
try {
std::vector<int> order = topological_sort();
std::unordered_set<std::string> failed_names;
int failed_count = 0;
for (int id : order) {
auto it = plugins_.find(id);
if (it == plugins_.end()) continue;
PluginInfo& plugin = it->second;
if (plugin.initialized) continue;
// 检查依赖是否已失败 / Check if dependency has already failed
bool dep_unavailable = false;
for (const auto& dep_name : plugin.dependencies) {
if (failed_names.count(dep_name)) {
dep_unavailable = true;
break;
}
}
if (dep_unavailable) {
host_api->log(DSTALK_LOG_WARN, "[plugin_loader] Plugin '%s' skipped: dependency unavailable",
plugin.name.c_str());
failed_names.insert(plugin.name);
failed_count++;
continue;
}
if (plugin.info->on_init) {
int result;
try {
result = plugin.info->on_init(host_api);
} catch (const std::exception& e) {
host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init threw: %s",
plugin.name.c_str(), e.what());
failed_names.insert(plugin.name);
failed_count++;
continue;
} catch (...) {
host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init threw unknown exception",
plugin.name.c_str());
failed_names.insert(plugin.name);
failed_count++;
continue;
}
if (result != 0) {
host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init failed (code %d)",
plugin.name.c_str(), result);
failed_names.insert(plugin.name);
failed_count++;
continue;
}
}
plugin.initialized = true;
}
return failed_count;
} catch (const std::runtime_error&) {
// 循环依赖 / Circular dependency
return -1;
} catch (const std::exception&) {
return -1;
}
}
// 仅初始化尚未初始化的插件(用于增量/按需加载)。
// 返回新初始化的插件数量,失败返回 -1。
// Initialize only plugins that haven't been initialized yet (used for incremental/on-demand loading).
// Returns the number of newly initialized plugins, or -1 on failure.
int PluginLoader::initialize_pending(const dstalk_host_api_t* host_api)
{
host_api_ = host_api;
try {
std::vector<int> order = topological_sort();
int count = 0;
for (int id : order) {
auto it = plugins_.find(id);
if (it == plugins_.end()) continue;
PluginInfo& plugin = it->second;
if (plugin.initialized) continue;
if (plugin.info->on_init) {
int result;
try {
result = plugin.info->on_init(host_api);
} catch (const std::exception& e) {
if (host_api) host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init threw: %s",
plugin.name.c_str(), e.what());
return -1;
} catch (...) {
if (host_api) host_api->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' init threw unknown exception",
plugin.name.c_str());
return -1;
}
if (result != 0) {
return -1;
}
}
plugin.initialized = true;
count++;
}
return count;
} catch (const std::exception&) {
return -1;
}
}
// 按逆依赖顺序关闭所有插件,然后释放所有 DLL 句柄并清空 map。
// Shutdown all plugins in reverse dependency order, then free all DLL handles and clear the map.
void PluginLoader::shutdown_all()
{
// 按逆序关闭 / Shutdown in reverse order
std::vector<int> order;
try {
order = topological_sort();
std::reverse(order.begin(), order.end());
} catch (...) {
// 如果排序失败,按任意顺序关闭 / If sorting fails, shutdown in arbitrary order
for (const auto& [id, _] : plugins_) {
order.push_back(id);
}
}
for (int id : order) {
auto it = plugins_.find(id);
if (it == plugins_.end()) continue;
PluginInfo& plugin = it->second;
if (plugin.initialized && plugin.info->on_shutdown) {
try {
plugin.info->on_shutdown();
} catch (const std::exception& e) {
if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' shutdown threw: %s",
plugin.name.c_str(), e.what());
} catch (...) {
if (host_api_) host_api_->log(DSTALK_LOG_ERROR, "[plugin_loader] Plugin '%s' shutdown threw unknown exception",
plugin.name.c_str());
}
}
plugin.initialized = false;
}
// 释放所有 DLL 句柄 / Free all DLL handles
for (auto& [id, plugin] : plugins_) {
if (plugin.handle) {
#ifdef _WIN32
FreeLibrary((HMODULE)plugin.handle);
#else
dlclose(plugin.handle);
#endif
plugin.handle = nullptr;
}
}
plugins_.clear();
}
// 按 ID 查找插件。返回 PluginInfo 指针,未找到则返回 nullptr。
// Look up a plugin by ID. Returns pointer to PluginInfo, or nullptr if not found.
const PluginInfo* PluginLoader::get_plugin(int plugin_id) const
{
auto it = plugins_.find(plugin_id);
if (it == plugins_.end()) return nullptr;
return &it->second;
}
} // namespace dstalk

View File

@@ -0,0 +1,76 @@
/*
* @file plugin_loader.hpp
* @brief DLL plugin loader with topological sort for dependency-ordered initialization.
* DLL 插件加载器,使用拓扑排序实现按依赖顺序初始化。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#pragma once
#include "dstalk/dstalk_host.h"
#include <atomic>
#include <string>
#include <unordered_map>
#include <vector>
namespace dstalk {
// 描述单个已加载插件标识、DLL 句柄、信息 vtable 和初始化状态。
// Describes a single loaded plugin: identity, DLL handle, info vtable, and init state.
struct PluginInfo {
int id;
std::string name;
std::string version;
std::string description;
int api_version;
std::vector<std::string> dependencies;
void* handle; // DLL 句柄 / DLL handle
dstalk_plugin_info_t* info;
bool initialized;
};
// 管理基于 DLL 的插件生命周期:加载、卸载、验证依赖、
// 拓扑排序初始化、关闭和 JSON 列表。
// Manages the lifecycle of DLL-based plugins: load, unload, validate dependencies,
// topological-sort initialization, shutdown, and JSON listing.
class PluginLoader {
public:
PluginLoader() = default;
~PluginLoader();
// 加载插件返回插件ID失败返回-1 / Load plugin (returns plugin ID, -1 on failure)
int load_plugin(const char* path);
// 卸载插件 / Unload plugin
int unload_plugin(int plugin_id);
// 获取插件列表JSON格式 / Get plugin list (JSON format)
std::string list_plugins() const;
// 按依赖顺序初始化所有插件 / Initialize all plugins in dependency order
int initialize_all(const dstalk_host_api_t* host_api);
// 仅初始化尚未初始化的插件(增量加载场景) / Initialize only uninitialized plugins (incremental loading scenario)
int initialize_pending(const dstalk_host_api_t* host_api);
// 关闭所有插件 / Shutdown all plugins
void shutdown_all();
// 获取插件信息 / Get plugin info
const PluginInfo* get_plugin(int plugin_id) const;
private:
// 拓扑排序(按依赖顺序) / Topological sort (by dependency order)
std::vector<int> topological_sort() const;
// 依赖合法性校验(缺失依赖 + 循环依赖),返回 0 成功 / -1 失败
// Validate dependencies (missing + circular), returns 0 success / -1 failure
int validate_dependencies() const;
std::unordered_map<int, PluginInfo> plugins_;
std::atomic<int> next_id_{1};
const dstalk_host_api_t* host_api_ = nullptr;
};
} // namespace dstalk

View File

@@ -0,0 +1,51 @@
/* @file service_registry.cpp
* @brief ServiceRegistry implementation: register, query, unregister with reader-writer locking.
* ServiceRegistry 实现:基于读写锁的 register、query、unregister。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include "service_registry.hpp"
namespace dstalk {
// 注册指定版本的命名服务 / Register a named service at a given version. 参数为空返回 -1已注册返回 -2 / Returns -1 on null args, -2 if already registered.
int ServiceRegistry::register_service(const char* name, int version, void* vtable)
{
if (!name || !vtable) return -1;
std::unique_lock<std::shared_mutex> lock(mutex_);
// 检查是否已注册 / Check if already registered
if (services_.find(name) != services_.end()) {
return -2; // 已存在 / already registered
}
services_[name] = {name, version, vtable};
return 0;
}
// 按名称和最低版本查询服务 / Query a service by name and minimum version. 返回 vtable 指针或 nullptr / Returns vtable pointer or nullptr if not found.
void* ServiceRegistry::query_service(const char* name, int min_version) const
{
if (!name) return nullptr;
std::shared_lock<std::shared_mutex> lock(mutex_);
auto it = services_.find(name);
if (it == services_.end()) return nullptr;
if (it->second.version < min_version) return nullptr;
return it->second.vtable;
}
// 注销指定名称的服务name 为空或未找到时无操作)/ Unregister a named service (no-op if name is null or not found).
void ServiceRegistry::unregister_service(const char* name)
{
if (!name) return;
std::unique_lock<std::shared_mutex> lock(mutex_);
services_.erase(name);
}
} // namespace dstalk

View File

@@ -0,0 +1,46 @@
/* @file service_registry.hpp
* @brief Name-versioned service registry for decoupled plugin communication.
* 基于名称+版本的服务注册表,用于插件间解耦通信。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#pragma once
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
namespace dstalk {
// 名称 + 最低版本服务目录 / Name + minimum-version service directory.
// 插件注册 vtable消费者按名称和版本约束查询 /
// Plugins register vtables; consumers query by name and version constraint.
// 读取query使用 shared_lock写入register/unregister使用 unique_lock /
// Reads (query) use shared_lock; writes (register/unregister) use unique_lock.
class ServiceRegistry {
public:
ServiceRegistry() = default;
~ServiceRegistry() = default;
// 注册服务 / Register a named service at a given version
int register_service(const char* name, int version, void* vtable);
// 查询服务(返回 vtable 指针,或 nullptr/ Query a service by name and minimum version
void* query_service(const char* name, int min_version) const;
// 注销服务 / Unregister a named service
void unregister_service(const char* name);
private:
struct ServiceEntry {
std::string name;
int version;
void* vtable;
};
mutable std::shared_mutex mutex_; // 读写锁query 用 sharedregister/unregister 用 unique / RW lock: shared for query, unique for register/unregister
std::unordered_map<std::string, ServiceEntry> services_;
};
} // namespace dstalk