diff --git a/docs/en/dev/server-plugin.md b/docs/en/dev/server-plugin.md new file mode 100644 index 00000000..c9707bae --- /dev/null +++ b/docs/en/dev/server-plugin.md @@ -0,0 +1,60 @@ +You can implement a clice server plugin to extend clice's functionality. + +## Use case + +When you use `clice` as lsp backend of LLM agents, e.g. claude code, you can add plugin to provide some extra features. + +## Writing a plugin + +When a plugin is loaded by the server, it will call `clice_get_server_plugin_info` to obtain information about this plugin and about how to register its customization points. + +This function needs to be implemented by the plugin, see the example below: + +```c++ +extern "C" ::clice::PluginInfo LLVM_ATTRIBUTE_WEAK +clice_get_server_plugin_info() { + return { + CLICE_PLUGIN_API_VERSION, "MyPlugin", "v0.1", CLICE_PLUGIN_DEF_HASH, + [](ServerPluginBuilder builder) { ... } + }; +} +``` + +See [PluginDef.h](/include/Server/PluginDef.h) for more details. + +## Getting content of `CLICE_PLUGIN_DEF_HASH` + +There are two values to return in the `clice_get_server_plugin_info` function. + +- `CLICE_PLUGIN_API_VERSION` is used to ensure compability of the `clice_get_server_plugin_info` function between the plugin and the server. +- `CLICE_PLUGIN_DEF_HASH` is used to ensure the consistency of the C++ declarations between the plugin and the server. + +To debug the content of `CLICE_PLUGIN_DEF_HASH`, you can run following command: + +```shell +$ git clone https://github.com/clice-io/clice.git +$ cd clice +$ git checkout `clice --version --git-describe` +$ python scripts/plugin-def.py content +``` + +You will get a C source code file, content of which is like this: + +```cpp +#if 0 +// begin of config/llvm-manifest.json +[ + { + "version": "21.1.4+r1", + "filename": "arm64-macos-clang-debug-asan.tar.xz", + "sha256": "7da4b7d63edefecaf11773e7e701c575140d1a07329bbbb038673b6ee4516ff5", + "lto": false, + "asan": true, + "platform": "macosx", + "build_type": "Debug" + }, + ... +] +... +#endif +``` diff --git a/docs/zh/dev/server-plugin.md b/docs/zh/dev/server-plugin.md new file mode 100644 index 00000000..e6d56469 --- /dev/null +++ b/docs/zh/dev/server-plugin.md @@ -0,0 +1,60 @@ +你可以在 clice 中实现一个 server plugin 来扩展 clice 的功能。 + +## 用例 + +当你使用 `clice` 作为 LLM 代理的 LSP 后端时,比如 claude code,你可以添加插件来提供一些额外功能。 + +## 编写一个插件 + +当一个插件被服务器加载时,它会调用 `clice_get_server_plugin_info` 来获取关于这个插件的信息以及如何注册它的定制点。 + +这个函数需要由插件实现,请参考下面的示例: + +```cpp +extern "C" ::clice::PluginInfo LLVM_ATTRIBUTE_WEAK +clice_get_server_plugin_info() { + return { + CLICE_PLUGIN_API_VERSION, "MyPlugin", "v0.1", CLICE_PLUGIN_DEF_HASH, + [](ServerPluginBuilder builder) { ... } + }; +} +``` + +请参考 [PluginDef.h](/include/Server/PluginDef.h) 了解更多细节。 + +## 获取 `CLICE_PLUGIN_DEF_HASH` 的内容 + +在 `clice_get_server_plugin_info` 函数中需要返回两个值。 + +- `CLICE_PLUGIN_API_VERSION` 用于确保插件和服务器之间的 `clice_get_server_plugin_info` 函数的一致性。 +- `CLICE_PLUGIN_DEF_HASH` 用于确保插件和服务器之间的 C++ 声明的一致性。 + +要调试 `CLICE_PLUGIN_DEF_HASH` 的内容,你可以运行以下命令: + +```shell +$ git clone https://github.com/clice-io/clice.git +$ cd clice +$ git checkout `clice --version --git-describe` +$ python scripts/plugin-def.py content > /tmp/plugin-proto.h +``` + +你将会得到一个 C 源码格式的文件,内容大致如下: + +```cpp +#if 0 +// begin of config/llvm-manifest.json +[ + { + "version": "21.1.4+r1", + "filename": "arm64-macos-clang-debug-asan.tar.xz", + "sha256": "7da4b7d63edefecaf11773e7e701c575140d1a07329bbbb038673b6ee4516ff5", + "lto": false, + "asan": true, + "platform": "macosx", + "build_type": "Debug" + }, + ... +] +... +#endif +``` diff --git a/include/Server/Plugin.h b/include/Server/Plugin.h new file mode 100644 index 00000000..952234b9 --- /dev/null +++ b/include/Server/Plugin.h @@ -0,0 +1,73 @@ +#pragma once +#include + +#include "Async/Async.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/JSON.h" + +// clang-format off +/// Run `python scripts/plugin-def.py update` to update the hash. +#define CLICE_PLUGIN_DEF_HASH "sha256:e35ff1adfbc385f7bae605e82ca4e7e70cfbf0f933bb8d57169d0d29abe5b6f7" +// clang-format on + +namespace clice { + +class Server; + +#define CLICE_PLUGIN_PROTOCOL + +#include "PluginDef.h" +#undef CLICE_PLUGIN_PROTOCOL + +struct ServerPluginBuilder; + +/// A loaded server plugin. +/// +/// An instance of this class wraps a loaded server plugin and gives access to its interface. +class Plugin { +public: + struct Self; + + Plugin(Self* self) : self(self) {} + + Self* operator->() { + return self; + } + +public: + /// Attempts to load a server plugin from a given file. + /// + /// Returns an error if either the library cannot be found or loaded, + /// there is no public entry point, or the plugin implements the wrong API + /// version. + static std::expected load(const std::string& file_path); + + /// Gets the file path of the loaded plugin. + llvm::StringRef file_path() const; + + /// Gets the name of the loaded plugin. + llvm::StringRef name() const; + + /// Gets the version of the loaded plugin. + llvm::StringRef version() const; + + /// Registers the server callbacks for the loaded plugin. + void register_server_callbacks(ServerPluginBuilder& builder) const; + +protected: + Self* self; +}; + +struct ServerPluginBuilder { +#define CliceServerPluginAPI(METHOD, ...) void METHOD(__VA_ARGS__) + +#include "PluginDef.h" +#undef CliceServerPluginAPI + +protected: + ServerRef server_ref; +}; + +} // namespace clice diff --git a/include/Server/PluginDef.h b/include/Server/PluginDef.h new file mode 100644 index 00000000..c14fc8f2 --- /dev/null +++ b/include/Server/PluginDef.h @@ -0,0 +1,81 @@ +/// The API version of the clice plugin. +/// Update this version when you change: +/// - The definition of struct `PluginInfo`. +/// - The definition of function `clice_get_server_plugin_info`. +/// Note: you don't have to update this version if you only change other APIs, which is guaranteed +/// by the `PluginInfo::definition_hash`. +#ifndef CLICE_PLUGIN_API_VERSION +#define CLICE_PLUGIN_API_VERSION 1 +#elif CLICE_PLUGIN_API_VERSION != 1 +#error "CLICE_PLUGIN_API_VERSION must be 1, but got " CLICE_PLUGIN_API_VERSION +#endif + +/// Defines the library APIs that loads a plugin. +#ifdef CLICE_PLUGIN_PROTOCOL +struct ServerPluginBuilder; +extern "C" { + /// A C-compatible struct that contains information about the plugin. + struct PluginInfo { + /// The clice API version of the plugin. + std::uint32_t api_version; + /// The name of the plugin. + const char* name; + /// The version of the plugin. + const char* version; + /// The plugin definition hash. + const char* definition_hash; + /// Registers the server callbacks for the loaded plugin. + void (*register_server_callbacks)(ServerPluginBuilder& builder); + }; + + /// The public entry point for a pass plugin. + /// + /// When a plugin is loaded by the server, it will call this entry point to + /// obtain information about this plugin and about how to register its customization points. + /// This function needs to be implemented by the plugin, see the example below: + /// + /// ``` + /// extern "C" ::clice::PluginInfo LLVM_ATTRIBUTE_WEAK + /// clice_get_server_plugin_info() { + /// return { + /// CLICE_PLUGIN_API_VERSION, "MyPlugin", "v0.1", CLICE_PLUGIN_DEF_HASH, + /// [](ServerPluginBuilder builder) { ... } + /// }; + /// } + /// ``` + PluginInfo LLVM_ATTRIBUTE_WEAK clice_get_server_plugin_info(); +} + +struct ServerRef { +public: + struct Self; + + ServerRef(Self* self) : self(self) {} + + Self* operator->() const { + return self; + } + + Server& server() const; + + /// Gets the configuration for the given section. + std::expected get_configuration(llvm::StringRef section); + +protected: + Self* self; +}; +#endif + +/// Defines the library APIs to register callbacks for a plugin. +#ifdef CliceServerPluginAPI +CliceServerPluginAPI(get_server_ref, ServerRef& server); +using configuration_handler_t = async::Task<> (*)(ServerRef server, void* plugin_data); +CliceServerPluginAPI(on_did_change_configuration, configuration_handler_t callback); +using command_handler_t = + async::Task (*)(ServerRef server, + void* plugin_data, + const llvm::ArrayRef& arguments); +CliceServerPluginAPI(register_commmand_handler, + llvm::StringRef command, + command_handler_t callback); +#endif diff --git a/include/Server/Server.h b/include/Server/Server.h index 9a2794ff..efd2836f 100644 --- a/include/Server/Server.h +++ b/include/Server/Server.h @@ -3,6 +3,7 @@ #include "Config.h" #include "Convert.h" #include "Indexer.h" +#include "Plugin.h" #include "Async/Async.h" #include "Compiler/Command.h" #include "Compiler/Diagnostic.h" @@ -113,7 +114,7 @@ private: class Server { public: - Server(); + Server(std::vector&& plugins); using Self = Server; @@ -241,6 +242,9 @@ private: config::Config config; Indexer indexer; + + /// All loaded server plugins. + std::vector plugins; }; } // namespace clice diff --git a/scripts/plugin-def.py b/scripts/plugin-def.py new file mode 100644 index 00000000..0e75d4e3 --- /dev/null +++ b/scripts/plugin-def.py @@ -0,0 +1,103 @@ +# read include/Server/PluginDef.h and substitute CLICE_PLUGIN_DEF_HASH in include/Server/Plugin.h +# hash is in `sha256:` format. + +import hashlib +import re +from pathlib import Path +import sys + +# + +clice_apis = [] + +# all of the files in `include/`, other than `include/Server/Plugin.h` +for path in Path("include/").glob("**/*.h"): + if path.name == "Plugin.h": + continue + clice_apis.append(path) + +clice_apis = [ + # the dependencies of the clice. + Path("config/llvm-manifest.json"), + # the clice C/C++ sources. + *sorted(clice_apis), +] + + +def fatal(message: str): + print(f"error: {message}", file=sys.stderr) + sys.exit(1) + + +def sha256sum(paths: list[Path]) -> str: + digest = hashlib.sha256() + for path in paths: + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def clice_api_content(): + content = [] + content.append("#if 0") + for path in clice_apis: + with path.open("r") as file: + content.append(f"// begin of {path}") + content.append(file.read()) + content.append(f"// end of {path}") + content.append("#endif") + return "\n".join(content) + + +def plugin_def_hash(): + hash_val = sha256sum(clice_apis) + return f"sha256:{hash_val}" + + +def update(): + hash_val = plugin_def_hash() + with open("include/Server/Plugin.h", "r") as file: + content = file.read() + content = re.sub( + r"#define CLICE_PLUGIN_DEF_HASH .*", + f'#define CLICE_PLUGIN_DEF_HASH "{hash_val}"', + content, + ) + with open("include/Server/Plugin.h", "w") as file: + file.write(content) + + +def check(): + hash_val = plugin_def_hash() + with open("include/Server/Plugin.h", "r") as file: + content = file.read() + match = re.search(r'#define CLICE_PLUGIN_DEF_HASH "(.*)"', content) + if match is None: + fatal("plugin def hash not found in include/Server/Plugin.h") + if match.group(1) != hash_val: + fatal( + f"plugin def hash mismatch in include/Server/Plugin.h, expected: {hash_val}, actual: {match.group(1)}" + ) + print(f"plugin def hash is up to date: {hash_val}") + + +def main(): + if len(sys.argv) > 1: + if sys.argv[1] == "update": + update() + check() + elif sys.argv[1] == "check": + check() + elif sys.argv[1] == "content": + print(clice_api_content()) + else: + fatal( + f"invalid command: {sys.argv[1]}, expected: update, check", + ) + else: + fatal("no command provided, expected: update, check") + + +if __name__ == "__main__": + main() diff --git a/src/Server/Lifecycle.cpp b/src/Server/Lifecycle.cpp index 02dd241d..448665ef 100644 --- a/src/Server/Lifecycle.cpp +++ b/src/Server/Lifecycle.cpp @@ -1,3 +1,4 @@ +#include "Server/Plugin.h" #include "Server/Server.h" #include diff --git a/src/Server/Plugin.cpp b/src/Server/Plugin.cpp new file mode 100644 index 00000000..66c8ba22 --- /dev/null +++ b/src/Server/Plugin.cpp @@ -0,0 +1,111 @@ +#include "Server/Plugin.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/Support/DynamicLibrary.h" + +namespace clice { + +struct ServerRef::Self { + Server* server; +}; + +Server& ServerRef::server() const { + return *self->server; +} + +struct Plugin::Self { + /// The file path of the plugin. + std::string file_path; + /// The dynamic library data of the plugin. + llvm::sys::DynamicLibrary library; + /// The name of the plugin. + std::string name; + /// The version of the plugin. + std::string version; + /// Registers the server callbacks for the loaded plugin. + void (*register_server_callbacks)(ServerPluginBuilder& builder); +}; + +std::expected Plugin::load(const std::string& file_path) { + std::string err; + auto library = llvm::sys::DynamicLibrary::getPermanentLibrary(file_path.c_str(), &err); + if(!library.isValid()) { + return std::unexpected("Could not load library '" + file_path + "': " + err); + } + + Plugin P{ + new Self{file_path, library} + }; + + /// `clice_get_server_plugin_info` should be resolved to the definition from the plugin + /// we are currently loading. + intptr_t get_details_fn = (intptr_t)library.getAddressOfSymbol("clice_get_server_plugin_info"); + + if(!get_details_fn) { + /// If the symbol isn't found, this is probably a legacy plugin, which is an + /// error. + return std::unexpected("Plugin entry point not found in '" + file_path + + "'. Is this a clice server plugin?"); + } + + auto info = reinterpret_cast(get_details_fn)(); + + /// First, we check whether the plugin is compatible with the clice plugin API. + if(info.api_version != CLICE_PLUGIN_API_VERSION) { + return std::unexpected("Wrong API version on plugin '" + file_path + "'. Got version " + + std::to_string(info.api_version) + ", supported version is " + + std::to_string(CLICE_PLUGIN_API_VERSION) + "."); + } + + /// Then, we safely get definition hash from the plugin, and check if it is consistent with + /// the expected hash. This ensures that the plugin has consistent declarations with the server. + std::string definition_hash = info.definition_hash; + if(definition_hash != CLICE_PLUGIN_DEF_HASH) { + return std::unexpected("Wrong definition hash on plugin '" + file_path + "'. Got '" + + definition_hash + "', expected '" + CLICE_PLUGIN_DEF_HASH + "'."); + } + + if(!info.register_server_callbacks) { + return std::unexpected("Empty `register_server_callbacks` function in plugin '" + + file_path + "'."); + } + + P->name = info.name; + P->version = info.version; + P->register_server_callbacks = info.register_server_callbacks; + + return P; +} + +llvm::StringRef Plugin::file_path() const { + return self->file_path; +} + +llvm::StringRef Plugin::name() const { + return self->name; +} + +llvm::StringRef Plugin::version() const { + return self->version; +} + +using command_handler_t = + async::Task (*)(ServerRef server, + const llvm::ArrayRef& arguments); + +void ServerPluginBuilder::get_server_ref(ServerRef& server) { + server = server_ref; +} + +void ServerPluginBuilder::on_did_change_configuration(configuration_handler_t callback) { + std::println(stderr, "on_changed_configuration"); + abort(); +} + +void ServerPluginBuilder::register_commmand_handler(llvm::StringRef command, + command_handler_t callback) { + std::println(stderr, "register_commmand_handler: {}", command); + abort(); +} + +} // namespace clice diff --git a/src/Server/Server.cpp b/src/Server/Server.cpp index 451ba367..5343d212 100644 --- a/src/Server/Server.cpp +++ b/src/Server/Server.cpp @@ -96,7 +96,8 @@ async::Task<> Server::registerCapacity(llvm::StringRef id, }); } -Server::Server() : indexer(database, config, kind) { +Server::Server(std::vector&& plugins) : + indexer(database, config, kind), plugins(std::move(plugins)) { register_callback<&Server::on_initialize>("initialize"); register_callback<&Server::on_initialized>("initialized"); register_callback<&Server::on_shutdown>("shutdown"); diff --git a/src/clice.cc b/src/clice.cc index f94e1298..56862a82 100644 --- a/src/clice.cc +++ b/src/clice.cc @@ -1,3 +1,4 @@ +#include "Server/Plugin.h" #include "Server/Server.h" #include "Server/Version.h" #include "Support/Format.h" @@ -73,6 +74,15 @@ cl::opt log_level{ cl::desc("The log level, default is info"), }; +cl::opt> plugin_paths{ + "plugin-path", + cl::cat(category), + cl::value_desc("string"), + cl::init(std::vector{}), + cl::desc("The server plugins to load"), + cl::CommaSeparated, +}; + } // namespace int main(int argc, const char** argv) { @@ -103,8 +113,17 @@ int main(int argc, const char** argv) { async::init(); + std::vector plugins; + for(auto& plugin_path: plugin_paths) { + auto plugin_instance = Plugin::load(plugin_path); + if(!plugin_instance) { + LOG_FATAL("Failed to load plugin {}: {}", plugin_path, plugin_instance.error()); + } + plugins.push_back(std::move(plugin_instance.value())); + } + /// The global server instance. - static Server instance; + static Server instance(std::move(plugins)); auto loop = [&](json::Value value) -> async::Task<> { co_await instance.on_receive(value); };