Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[workspace]
members = ["mill-io", "mill-net"]
members = [
"mill-io",
"mill-net",
"mill-rpc",
"mill-rpc/mill-rpc-core",
"mill-rpc/mill-rpc-macros",
]
resolver = "2"

[workspace.package]
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@ A lightweight, production-ready event loop library for Rust that provides effici
- **Thread pool integration**: Configurable worker threads for handling I/O events
- **Compute pool**: Dedicated priority-based thread pool for CPU-intensive tasks
- **High-level networking**: High-level server/client components based on mill-io
- **RPC framework**: macro-driven RPC with type-safe clients and servers
- **Object pooling**: Reduces allocation overhead for frequent operations
- **Clean API**: Simple registration and handler interface

## Crates

| Crate | Description |
| ------------------------ | -------------------------------------------- |
| [`mill-io`](./mill-io) | Core reactor-based event loop |
| [`mill-net`](./mill-net) | High-level TCP server/client networking |
| [`mill-rpc`](./mill-rpc) | Macro-driven RPC framework (server + client) |

## Installation

For the core event loop only:
Expand All @@ -28,6 +37,13 @@ For high-level networking (includes mill-io as dependency):
mill-net = "2.0.1"
```

For the RPC framework (includes mill-io and mill-net):

```toml
[dependencies]
mill-rpc = { path = "mill-rpc" }
```
Comment on lines +40 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider updating the install snippet once published.

The path-based dependency mill-rpc = { path = "mill-rpc" } is correct for development but won't work for external users. Update this to a version-based dependency once the crate is published to crates.io.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 40 - 45, Replace the development-only path dependency
declaration mill-rpc = { path = "mill-rpc" } in the README with a version-based
crate dependency once mill-rpc is published to crates.io; update the install
snippet to use the published version string (e.g., mill-rpc = "x.y.z") and
mention that users should pin the appropriate version, so external users can add
the crate from crates.io instead of relying on a local path.


## Architecture

For detailed architectural documentation, see [Architecture Guide](./docs/Arch.md).
Expand Down
63 changes: 63 additions & 0 deletions mill-rpc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[package]
name = "mill-rpc"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "An Axum-inspired RPC framework built on Mill-IO"

[dependencies]
mill-rpc-core = { path = "mill-rpc-core" }
mill-rpc-macros = { path = "mill-rpc-macros" }
mill-io = { path = "../mill-io" }
mill-net = { path = "../mill-net" }
serde = { version = "1", features = ["derive"] }
bincode = "1"
log = "0.4"
mio = { version = "1", features = ["os-poll", "net"] }

[dev-dependencies]
env_logger = "0.11"
criterion = { version = "0.5", features = ["html_reports"] }
serde_cbor = "0.11"

[[example]]
name = "calculator_server"
path = "examples/calculator_server.rs"

[[example]]
name = "calculator_client"
path = "examples/calculator_client.rs"

[[example]]
name = "echo_server"
path = "examples/echo_server.rs"

[[example]]
name = "echo_client"
path = "examples/echo_client.rs"

[[example]]
name = "kv_server"
path = "examples/kv_server.rs"

[[example]]
name = "kv_client"
path = "examples/kv_client.rs"

[[example]]
name = "multi_service_server"
path = "examples/multi_service_server.rs"

[[example]]
name = "multi_service_client"
path = "examples/multi_service_client.rs"

[[example]]
name = "concurrent_clients"
path = "examples/concurrent_clients.rs"

[[bench]]
name = "rpc_comparison"
harness = false
138 changes: 138 additions & 0 deletions mill-rpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# mill-rpc

An RPC framework built on [`mill-io`](../mill-io) and [`mill-net`](../mill-net). Define services declaratively, get type-safe clients and servers — no async runtime required.

## Features

- **Zero async**: Handlers are plain synchronous functions
- **Macro-driven**: `mill_rpc::service!` generates a module with server trait, client struct, and dispatch logic
- **Selective generation**: Use `#[server]`, `#[client]`, or both (default)
- **Multi-service**: Host multiple services on a single port with automatic routing
- **Pluggable codecs**: Bincode by default, extensible
- **Binary wire protocol**: Efficient framing with one-way calls and ping/pong

## Quick Start

### Define a service

```rust
mill_rpc::service! {
service Calculator {
fn add(a: i32, b: i32) -> i32;
fn multiply(a: i64, b: i64) -> i64;
}
}
```

This generates a `calculator` module containing:
- `calculator::Service`: trait to implement on the server
- `calculator::server(impl)`: wraps your impl for registration
- `calculator::Client`: struct with typed RPC methods
- `calculator::methods`: method ID constants

### Server

```rust
struct MyCalc;

impl calculator::Service for MyCalc {
fn add(&self, _ctx: &RpcContext, a: i32, b: i32) -> i32 { a + b }
fn multiply(&self, _ctx: &RpcContext, a: i64, b: i64) -> i64 { a * b }
}

fn main() {
let event_loop = Arc::new(EventLoop::new(4, 1024, 100).unwrap());

let _server = RpcServer::builder()
.bind("127.0.0.1:9001".parse().unwrap())
.service(calculator::server(MyCalc))
.build(&event_loop)
.unwrap();

event_loop.run().unwrap();
}
```
Comment on lines +43 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider documenting the EventLoop parameters.

The example uses EventLoop::new(4, 1024, 100) without explaining what these parameters represent. Adding a brief inline comment would help readers understand the configuration.

📝 Suggested improvement
 fn main() {
-    let event_loop = Arc::new(EventLoop::new(4, 1024, 100).unwrap());
+    // 4 worker threads, 1024 max events per poll, 100ms timeout
+    let event_loop = Arc::new(EventLoop::new(4, 1024, 100).unwrap());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn main() {
let event_loop = Arc::new(EventLoop::new(4, 1024, 100).unwrap());
let _server = RpcServer::builder()
.bind("127.0.0.1:9001".parse().unwrap())
.service(calculator::server(MyCalc))
.build(&event_loop)
.unwrap();
event_loop.run().unwrap();
}
```
fn main() {
// 4 worker threads, 1024 max events per poll, 100ms timeout
let event_loop = Arc::new(EventLoop::new(4, 1024, 100).unwrap());
let _server = RpcServer::builder()
.bind("127.0.0.1:9001".parse().unwrap())
.service(calculator::server(MyCalc))
.build(&event_loop)
.unwrap();
event_loop.run().unwrap();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mill-rpc/README.md` around lines 43 - 54, Add a brief inline comment in the
README example next to EventLoop::new(4, 1024, 100) explaining what each numeric
argument represents (e.g., thread count, max connections/queue size, timeout or
max events) so readers understand the configuration; update the line in the main
example where EventLoop::new(...) is called (the EventLoop::new(4, 1024, 100)
invocation inside fn main) with a short comment describing each parameter.


### Client

```rust
let transport = RpcClient::connect(addr, &event_loop).unwrap();
let client = calculator::Client::new(transport, Codec::bincode(), 0);

let sum = client.add(10, 25).unwrap(); // 35
let prod = client.multiply(7, 8).unwrap(); // 56
```

## Selective Generation

Generate only what you need:

```rust
// Server crate: no client code generated
mill_rpc::service! {
#[server]
service Calculator {
fn add(a: i32, b: i32) -> i32;
}
}

// Client crate: no server code generated
mill_rpc::service! {
#[client]
service Calculator {
fn add(a: i32, b: i32) -> i32;
}
}

// Both (default): for tests, examples, or single-binary apps
mill_rpc::service! {
service Calculator {
fn add(a: i32, b: i32) -> i32;
}
}
```

## Multi-Service Server

```rust
mill_rpc::service! {
#[server]
service MathService {
fn factorial(n: u64) -> u64;
}
}

mill_rpc::service! {
#[server]
service StringService {
fn reverse(s: String) -> String;
}
}

let _server = RpcServer::builder()
.bind(addr)
.service(math_service::server(MathImpl)) // service_id = 0
.service(string_service::server(StringImpl)) // service_id = 1
.build(&event_loop)?;

// Client side: share one connection
let math = math_service::Client::new(transport.clone(), codec, 0);
let strings = string_service::Client::new(transport, codec, 1);
```

## Examples

```bash
# Terminal 1 # Terminal 2
cargo run --example calculator_server cargo run --example calculator_client
cargo run --example echo_server cargo run --example echo_client
cargo run --example kv_server cargo run --example kv_client
cargo run --example multi_service_server cargo run --example multi_service_client

# Self-contained stress test
cargo run --example concurrent_clients
```

## License

Licensed under the Apache License, Version 2.0. See [LICENSE](../LICENSE) for details.
Loading
Loading