Hotaru 是一个采用宏语法的 Rust Web 框架,将 URL 、中间件、配置和处理器整合在一个代码块中。如果你正在用 Rust 构建 Web 服务,并且觉得属性宏分散各处不够优雅,这个框架可能适合你。希望获得反馈:endpoint!/middleware! 语法是否直观,还是隐藏了太多细节?
仓库地址: https://github.com/Field-of-Dreams-Studio/hotaru
文档: https://fds.rs/hotaru/tutorial/0.7.3/
我是从 Python 转过来的,之前用 Flask 、FastAPI 那一套。当我转向 Rust 时,安全性的承诺打动了我——内存安全、没有空指针异常、编译器在运行前就能捕获 bug 。这些确实兑现了。
但当我开始研究 Web 框架时,发现了一个让我不太满意的模式:
#[get("/users/<id>")]
#[middleware::auth]
#[middleware::rate_limit(100)]
async fn get_user(...) -> impl Responder {
属性宏这种方式确实可行,很多人用它来交付生产环境的应用。但对我个人而言,配置分散在函数上方的感觉和我之前想要摆脱的装饰器模式很像。而且你还需要在别的地方手动注册路由。我想要的是能在一个地方看到端点的所有信息,并且自动完成注册。
所以我们尝试了一种不同的方式。
我们围绕一个核心理念构建了 Hotaru:端点的所有信息都应该在一起,并且自动注册。URL 、中间件、配置、处理器——一个代码块,一目了然,定义即注册。
endpoint! {
APP.url("/users/<int:id>"),
middleware = [.., auth_check, rate_limit],
config = [HttpSafety::new().with_allowed_methods(vec![GET, POST])],
pub get_user <HTTP> {
let user_id = req.param("id").unwrap_or_default();
json_response(object!({
id: user_id,
message: "User endpoint"
}))
}
}
这就是完整的语法。URL 模式(支持类型化参数如 <int:id>)、中间件栈、安全配置和处理器主体——全部在一个地方。req.param("id") 返回的值可以调用 .unwrap_or_default() 或 .string() 获取原始字符串。无需单独的注册步骤。宏在编译时展开为标准的异步 Rust 代码。
注意: 我们正在测试一种新的语法风格,可以使用更接近 Rust 函数的写法并自定义请求变量名。这个功能还在试验阶段,欢迎反馈。
endpoint! {
APP.url("/new_syntax/<arg>"),
middleware: [..],
config: ["ConfigString"],
pub fn new_syntax_endpoint(ctx: HTTP) {
let arg = ctx.pattern("arg").unwrap_or_default();
text_response(format!("New syntax endpoint called with arg: {}", arg))
}
}
两种写法在底层编译为相同的代码。我们想知道:同时支持两种风格是提高了清晰度,还是引入了不必要的不一致性?
定义端点的同时自动注册。不需要手动 router.register(),不会忘记注册,也不用到处找路由在哪里组装的。这是大多数 Rust Web 框架缺少的功能。
在大多数框架中定义中间件需要实现 trait 、包装服务、处理返回 future 的 future 。相当繁琐。
在 Hotaru 中:
middleware! {
pub LogRequest <HTTP> {
println!("[LOG] {} {}", req.method(), req.path());
let start = std::time::Instant::now();
let result = next(req).await;
println!("[LOG] Completed in {:?}", start.elapsed());
result
}
}
就这样。你拿到 req(即 HttpContext),调用 next(req).await 继续执行链,可以在返回时修改结果。想要短路?不调用 next() 就行:
middleware! {
pub AuthCheck <HTTP> {
let token = req.headers().get("Authorization");
if token.is_none() {
req.response = json_response(object!({
error: "unauthorized"
})).status(StatusCode::UNAUTHORIZED);
return req;
}
// 通过 locals 向下游传递类型化数据
req.locals.set("user_id", "user-123".to_string());
next(req).await
}
}
新的 fn 风格也支持中间件(试验中):
middleware! {
pub fn Logger(req: HTTP) {
println!("[LOG] {} {}", req.method(), req.path());
next(req).await
}
}
.. 模式这里我们借鉴了 Rust 结构体更新语法的设计。在大多数框架中,中间件组合要么全有要么全无,要么需要在构建器链中仔细排序。
// 应用级全局中间件
pub static APP: SApp = Lazy::new(|| {
App::new()
.binding("127.0.0.1:3000")
.append_middleware::<Logger>()
.append_middleware::<Metrics>()
.build()
});
// 仅使用全局中间件
endpoint! {
APP.url("/health"),
middleware = [..],
pub health <HTTP> { text_response("ok") }
}
// 全局 + 认证
endpoint! {
APP.url("/api/users"),
middleware = [.., auth_required],
pub users <HTTP> { /* ... */ }
}
// 三明治结构:timing 先执行,然后是全局中间件,最后是缓存检查
endpoint! {
APP.url("/api/cached"),
middleware = [timing, .., cache_layer],
pub cached <HTTP> { /* ... */ }
}
// 完全跳过全局中间件
endpoint! {
APP.url("/raw"),
middleware = [custom_only],
pub raw <HTTP> { /* ... */ }
}
.. 会展开为你的全局中间件。你可以在它前面、后面添加内容,或者完全跳过。只需查看端点定义,就能准确知道每个路由会执行哪些中间件。
这最初是一个实验,后来成为了架构的核心。Hotaru 可以在同一个端口上提供 HTTP 、WebSocket 和自定义 TCP 协议服务。
(我们实际上正在将这部分封装成更简洁的宏——新语法即将推出)
pub static APP: SApp = Lazy::new(|| {
App::new()
.binding("127.0.0.1:3000")
.handle(
HandlerBuilder::new()
.protocol(ProtocolBuilder::new(HTTP::server(HttpSafety::default())))
.protocol(ProtocolBuilder::new(WebSocketProtocol::new()))
.protocol(ProtocolBuilder::new(CustomProtocol::new()))
)
.build()
});
框架会检查传入的字节并路由到正确的处理器。REST API 、WebSocket 、自定义二进制协议——同一端口,共享状态。
无论什么协议,处理器看起来都一样:
endpoint! {
APP.url("/chat"),
pub chat_http <HTTP> {
html_response(include_str!("chat.html"))
}
}
endpoint! {
APP.url("/chat"),
pub chat_ws <WebSocket> {
// 相同 URL ,不同协议
ws.on_message(|msg| { /* ... */ }).await
}
}
我们提供了一个轻量级工具 crate 叫 Akari ,用于处理 JSON 和模板,无需引入 serde 。object! 宏可以内联构建 JSON:
json_response(object!({
status: "success",
data: {
users: [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}],
total: 2
}
}))
不需要派生宏,不需要为临时响应定义结构体。如果你想用 serde 也可以——不是禁止使用,只是不强制要求。
我们进行了一些初步基准测试,以验证宏方法不会增加运行时开销。这些是单机上的早期数据——仅供参考方向,不作为定论。
| 框架 | 请求数/秒 (JSON) | 相对性能 |
|---|---|---|
| Hotaru | 173,254 | 100% |
| Rocket | 171,904 | 99.2% |
| Actix-web | 149,244 | 86.1% |
| Axum | 148,934 | 86.0% |
在 Apple M 系列芯片上测试,单线程,简单 JSON 响应。我们将很快发布完整的测试方法和代码。
宏展开后就是你手写的普通异步函数。编译器看到的是展开后的普通 Rust 代码,并相应地进行优化。
在大多数 Rust Web 框架中,端点配置是这样的:
#[get("/users/<id>")]
#[middleware::auth]
#[middleware::rate_limit(100)]
async fn get_user(...) -> impl Responder {
// 处理器代码
}
// 然后在别的地方,你还需要注册它:
// router.register(get_user)
配置分散在函数上方。注册在别的地方。要理解一个端点做什么——以及它是否真的生效——你需要检查多个地方。
一个代码块。一个地方。URL 、中间件、配置、处理器——以及自动注册。
自动注册:定义端点的同时就注册了。不需要手动 router.register(),不会忘记注册,不用到处找路由在哪里组装的。这是大多数 Rust Web 框架缺少的功能。
一眼看清:端点的所有信息都在一个地方可见。
中间件组合:借鉴 Rust 的结构体更新语法——准确看到每个路由运行哪些中间件。
简化的中间件定义:没有 trait 样板代码。
多协议支持:相同 URL ,不同协议——语法一致。
我们承认当前的局限性:
| 局限性 | 状态 |
|---|---|
| 自定义语法有学习曲线 | 编译为标准异步 Rust |
| rustfmt 支持有限 | 计划中:自定义格式化工具 |
| IDE 支持参差不齐 | 已针对 rust-analyzer 优化 |
我们正在构建一个完整的工具链——包括我们自己的格式化工具和增强的 IDE 支持。Hotaru 正在从一个框架演变为一个带有完整工具的 Web 端点 DSL 。
所有宏都在编译时展开。零运行时开销——编译器在展开后看到的是普通 Rust 。
我们目前是 v0.7 版本,还在起步阶段。API 正在趋于稳定但还没有冻结。文档在改进中但仍有缺口。与成熟框架相比,生态系统还很小。
关于测试: 由于我们的数据库实现仍然非常抽象,目前的基准测试只能覆盖基础的 HTTP 响应场景。我们正在完善数据库层的设计,之后会进行更全面的测试。
我们的定位是一个对语法有独特见解的框架。我们认为处理器代码应该读起来就像它做的事情一样直观。
use hotaru::prelude::*;
use hotaru::http::*;
pub static APP: SApp = Lazy::new(|| {
App::new().binding("127.0.0.1:3000").build()
});
endpoint! {
APP.url("/"),
pub index <HTTP> {
text_response("Hello from Hotaru")
}
}
#[tokio::main]
async fn main() {
APP.clone().run().await;
}
仓库地址: https://github.com/Field-of-Dreams-Studio/hotaru
文档: https://fds.rs/hotaru/tutorial/0.7.3/
endpoint!/middleware! 语法感觉直观吗,还是隐藏了太多东西?fn 风格语法(试验中)有帮助吗,还是引入了不必要的复杂性?.. 中间件模式巧妙还是令人困惑?我们押注的是:用一点宏魔法换取更少的样板代码和自动注册是值得的。
我们是一个小团队,正在构建一些有野心的东西。如果你对以下方面感兴趣:
我们很乐意听到你的声音。访问我们的 GitHub 或者提 issue 开始对话。