Skip to content

Service — 跨模组服务

具名的请求 / 响应调用。问一个问题拿一个答案

rust
use levilamina::service;

// 提供服务
service::register("mymod:ping", |_name, request| {
    if request.is_empty() { return Err("需要一个名字".into()); }
    Ok(format!(r#"{{"pong":"{request}"}}"#))
})?.forget();

// 调用别人的
match service::call("plot:at", r#"{"dim":3,"x":10,"z":-4}"#) {
    Ok(reply) => println!("{reply}"),
    Err(service::CallError::NotFound) => println!("没装地皮插件"),
    Err(e) => println!("调用失败:{e}"),
}

和 Bus 的分工

BusService
同名提供者任意多个恰好一个
没人注册时正常一个必须处理的错误
返回值没有就是重点
顺序未定义,且必须无所谓只有一个被调方

总线用来宣布「发生了什么」,服务用来问「一个问题」。

API

API说明
service::register(name, handler)注册,返回 Result<Registration>
service::call(name, request)调用,返回 Result<String, CallError>
service::exists(name) -> bool现在有没有人提供
service::list_json() -> String全部服务,原始 JSON [{"name":…,"mod":…}]
reg.forget()活到模组卸载

处理函数签名:FnMut(&str, &str) -> Result<String, String>,即 (name, request) -> Ok(响应) | Err(错误说明)

注册是排他的

两个模组都提供 plot:can 不是"都跑",而是一个没法让调用方挑选的歧义答案。所以 register 直接失败,加载器日志里会写清楚是谁占着这个名字。

静默的"后者覆盖前者"会让答案取决于模组加载顺序——那个顺序没人控制,而且任何人装一个无关模组的那天它就变了。

命名空间

plot:can,不是 can。因为注册排他,裸名字撞车会变成启动时的硬失败,而不是运行时一个微妙的错误。这算是好事。

同步执行,没有超时

提供方在你的线程上直接跑完,答案从 call 返回。

不会有超时机制:会阻塞的提供方阻塞服务器线程的方式和任何别的回调完全一样,而返回"超时了"却让回调继续跑,等于同时给了你一个错误答案和一个还在运行的提供方。

不能调自己的服务

call 会拒绝。自己调自己等于绕两次 FFI 加一把互斥锁去到一个本来直接调用就行的函数,而当它真的构成循环时,栈是最难读的那种形状。

跨模组的循环(A → B → A)由深度上限拦住。

CallError

rust
pub enum CallError {
    NotFound,           // 没人提供这个服务
    Provider(String),   // 提供方返回了 Err
    Refused,            // 被拒绝(自调用、深度超限)
    Unknown(i32),
}

NotFound 是要处理的情况,不是要报错的情况

提供方模组没装就是 NotFound。这是"对方不在",不是"出问题了"——照常走降级逻辑,别往日志里打 error。

exists 是有竞态的

提供方可能在 existscall 之间卸载。用它来决定"要不要费劲构造一个昂贵的请求"可以,用它当保护伞不行——NotFound 该处理还是要处理。