# telepath-macros
Proc-macro crate providing the `#[command]` attribute that registers a plain Rust
function as a Telepath RPC command — no hand-written dispatch boilerplate required.
This crate is a **build-time only** dependency of `telepath-server`. It does not
appear in device firmware at runtime.
## Usage
```rust
use telepath_server::command;
// Plain command — all args are wire args
#[command]
fn ping() -> u32 {
0xDEAD_BEEF
}
#[command]
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Resource injection — peripheral state passed from ResourceRegistry, not over the wire
#[command]
fn set_led(#[resource] led: &mut MyLed, on: bool) -> bool {
led.set(on)
}
```
The `#[command]` attribute is re-exported from `telepath-server`; no direct
dependency on `telepath-macros` is required in downstream crates.
## Generated items
For each `#[command] fn foo(…) -> R` the macro emits:
| `fn __telepath_shim_foo(input: &[u8], output: &mut [u8]) -> Result<usize, DispatchError>` | Type-erased shim: deserializes args via postcard, calls `foo`, serializes return value |
| `fn __telepath_args_schema_foo(out: &mut [u8]) -> Result<usize, ()>` | Writes postcard-encoded `NamedType` schema for the argument tuple |
| `fn __telepath_ret_schema_foo(out: &mut [u8]) -> Result<usize, ()>` | Writes postcard-encoded `NamedType` schema for the return type |
| `pub const __TELEPATH_CMD_FOO: CommandMetadata` | Const holding name, `cmd_id`, and function pointers |
| `static __TELEPATH_REG_FOO: CommandMetadata` | `#[linkme::distributed_slice]` registration — zero-cost link-time registration in `TELEPATH_COMMANDS` |
The original function body is preserved unchanged and remains directly callable.
## Signature contract
**Allowed:**
- Free functions only (no `self` receiver)
- Any number of positional arguments with simple identifier patterns
- Wire argument types: any `T: Serialize + DeserializeOwned + postcard_schema::Schema` (owned, no references)
- `#[resource]`-annotated arguments: `&T` or `&mut T` where `T: 'static` — injected from the server's `ResourceRegistry`, **not** deserialized from the wire
- Wire and resource arguments may appear in any order
- Return type: any `T: Serialize + postcard_schema::Schema` (owned, no references); `()` means "no payload"
**Rejected at compile time:**
- `async fn`, `unsafe fn`
- Generic parameters or `where` clauses
- `&T` / `&mut T` argument **without** the `#[resource]` attribute
- `&T` / `&mut T` return type
- Methods (`fn foo(&self, …)`)
- Non-identifier argument patterns (e.g. tuple destructuring `(a, b): (i32, i32)`)
- Duplicate `#[resource]` types (each resource type may appear at most once)
## Wire encoding
- **Args:** only non-`#[resource]` arguments are serialized; resource arguments are server-side only. Serialized as a postcard tuple — `()` (zero wire args), `(T,)` (one wire arg), `(T1, T2, …)` (N wire args).
- **Return value:** serialized standalone (no wrapper tuple)
- **`cmd_id`:** derived deterministically from `(name, args_type_str, ret_type_str)` using **wire args only** via FNV-1a 16-bit. Adding or removing a `#[resource]` argument does **not** change the wire `cmd_id`. Renaming a function or changing a wire argument type is a **breaking wire change**.
- Reserved `cmd_id = 0x0000` is avoided by deterministic salt rehashing in `derive_cmd_id`; the discovery ID is never emitted by user commands
## Build
```bash
# Built automatically as part of the workspace
cargo build --workspace
# Inspect macro expansion in a consumer crate (requires cargo-expand)
cd telepath-server && cargo expand --test macro_smoke
```
`telepath-macros` is a workspace member targeting the native host (proc-macro crates
run on the build host, not the embedded target). No cross-compilation is needed.
## Toolchain
Stable Rust (pinned in `rust-toolchain.toml` at the repo root).
Changes MUST NOT break existing callers on the pinned stable channel.