feat: add minimal support to clice server plugins

This commit is contained in:
Myriad-Dreamin
2026-01-11 02:22:48 +08:00
parent c0ffd2369b
commit a118c16e96
10 changed files with 516 additions and 3 deletions

View File

@@ -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
```

View File

@@ -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
```

73
include/Server/Plugin.h Normal file
View File

@@ -0,0 +1,73 @@
#pragma once
#include <expected>
#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<Plugin, std::string> 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

View File

@@ -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<llvm::json::Value, std::string> 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<llvm::json::Value> (*)(ServerRef server,
void* plugin_data,
const llvm::ArrayRef<llvm::StringRef>& arguments);
CliceServerPluginAPI(register_commmand_handler,
llvm::StringRef command,
command_handler_t callback);
#endif

View File

@@ -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<Plugin>&& plugins);
using Self = Server;
@@ -241,6 +242,9 @@ private:
config::Config config;
Indexer indexer;
/// All loaded server plugins.
std::vector<Plugin> plugins;
};
} // namespace clice

103
scripts/plugin-def.py Normal file
View File

@@ -0,0 +1,103 @@
# read include/Server/PluginDef.h and substitute CLICE_PLUGIN_DEF_HASH in include/Server/Plugin.h
# hash is in `sha256:<hash>` 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()

View File

@@ -1,3 +1,4 @@
#include "Server/Plugin.h"
#include "Server/Server.h"
#include <llvm/Support/FileSystem.h>

111
src/Server/Plugin.cpp Normal file
View File

@@ -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, std::string> 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<decltype(clice_get_server_plugin_info)*>(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<llvm::json::Value> (*)(ServerRef server,
const llvm::ArrayRef<llvm::StringRef>& 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

View File

@@ -96,7 +96,8 @@ async::Task<> Server::registerCapacity(llvm::StringRef id,
});
}
Server::Server() : indexer(database, config, kind) {
Server::Server(std::vector<Plugin>&& 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");

View File

@@ -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<logging::Level> log_level{
cl::desc("The log level, default is info"),
};
cl::opt<std::vector<std::string>> plugin_paths{
"plugin-path",
cl::cat(category),
cl::value_desc("string"),
cl::init(std::vector<std::string>{}),
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<Plugin> 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);
};