feat: add minimal support to clice server plugins
This commit is contained in:
60
docs/en/dev/server-plugin.md
Normal file
60
docs/en/dev/server-plugin.md
Normal 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
|
||||
```
|
||||
60
docs/zh/dev/server-plugin.md
Normal file
60
docs/zh/dev/server-plugin.md
Normal 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
73
include/Server/Plugin.h
Normal 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
|
||||
81
include/Server/PluginDef.h
Normal file
81
include/Server/PluginDef.h
Normal 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
|
||||
@@ -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
103
scripts/plugin-def.py
Normal 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()
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "Server/Plugin.h"
|
||||
#include "Server/Server.h"
|
||||
|
||||
#include <llvm/Support/FileSystem.h>
|
||||
|
||||
111
src/Server/Plugin.cpp
Normal file
111
src/Server/Plugin.cpp
Normal 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
|
||||
@@ -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");
|
||||
|
||||
21
src/clice.cc
21
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<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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user