# ipcez
Rust library for **inter-process communication (IPC)** with deployment and platform flexibility: it detects whether the process you talk to is **local** or **remote** and picks the right transport (WebSocket, Unix domain socket, or Windows named pipe) based on the **operating system** and **backend deployment**.
**Recommendation:** Use **`socket_init(endpoint)`** (see [Socket API](#socket-api)): pass one endpoint string and set `target` / `os` in the environment for local connections, or use a `ws://` / `wss://` URL for remote. You do not need to know which transport is used.
## Requirements
- Rust 1.93+ (edition 2024)
- [Tokio](https://tokio.rs) runtime when using the async socket API (ipcez depends on tokio)
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
ipcez = "0.1"
```
If you use `socket_init`, ensure your binary has a Tokio runtime (e.g. `#[tokio::main]` or `tokio::runtime::Runtime`).
---
## Environment variables
The library uses two optional environment variables to choose transport. Set them in your deployment or config so the same binary can run locally or remotely.
| Variable | Allowed values | Meaning |
|-----------|------------------|--------|
| `os` | `linux`, `windows` | Operating system (case-insensitive). |
| `target` | `local`, `remote` | Whether the other process is on this machine or a remote server. |
For local endpoints, `socket_init(endpoint)` reads them automatically and returns a clear error (`SocketError::Detection`) if `os` or `target` is missing or invalid (the error message states which variable is the problem and, for invalid values, what is expected). When detection fails, the library also prints guidance to the console (stderr) explaining the missing or invalid variable and what to do to fix it.
---
## Socket API
### Socket API
You connect with **one** function and **one** endpoint string. Set the `target` and `os` environment variables for local connections; use a **WebSocket URL** for remote. No need to know which transport (WebSocket, Unix socket, or named pipe) is used.
**Endpoint rule:**
- **URL** (`ws://` or `wss://`) → connects as **remote** (WebSocket).
- **Otherwise** → treated as **local**: the library reads `target` and `os` from the environment. On Windows (local), if the string does not start with `\\.\pipe\`, that prefix is added automatically.
**Rust:** `socket_init(endpoint)`. **Node/TS:** `connect(addr)`. **Python:** `ipcez.connect(addr)`. **C#:** `Ipcez.Connect(addr)`.
**Rust example:**
```rust
use ipcez::{socket_init, Socket};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set env: target=local, os=windows (or linux). For remote, use a ws:// or wss:// URL.
let socket = socket_init("ipcez").await?; // or "ws://localhost:8080/" for remote
let (tx, mut rx) = mpsc::unbounded_channel();
socket.message_handler(move |result| {
let _ = tx.send(result);
async {}
});
socket.send_message(b"hello").await?;
if let Some(Ok(msg)) = rx.recv().await {
println!("received {} bytes", msg.len());
}
Ok(())
}
```
**Python example:**
```python
import os
os.environ["target"] = "local"
os.environ["os"] = "windows"
import ipcez
with ipcez.connect("ipcez") as sock:
sock.write(b"hello")
data = sock.read(256)
```
### Transport selection
`socket_init(endpoint)` resolves transport from the endpoint and (for local) from the `target` and `os` environment variables:
| Target | OS | Transport | Endpoint format |
|---------|---------|------------------|-----------------|
| Remote | any | WebSocket | `ws://host:port/path` or `wss://...` |
| Local | Linux | Unix domain socket | Socket path, e.g. `/tmp/ipcez.sock` |
| Local | Windows | Named pipe | `\\.\pipe\pipename` or just `pipename` |
Unsupported combinations (e.g. local + Linux on a Windows build) return `SocketError::UnsupportedCombination(target, os)`.
### Address format by transport
- **WebSocket (remote):** Full URL. Use `ws://` for plain, `wss://` for TLS.
- **Unix (local + Linux):** Absolute or relative path to the socket file (e.g. `/tmp/ipcez.sock`).
- **Named pipe (local + Windows):** Either the full path `\\.\pipe\pipename` or only `pipename`; the library prefixes `\\.\pipe\` when needed. After each `send_message()`, the library signals the named event `Global\pipename.data_ready` so a receiver can wait on it before reading, then waits up to 5 seconds for the receiver to signal `Global\pipename.data_acked` to confirm receipt. External recipients must signal the same-named "data acked" event after each successful read.
### Using the socket
`Socket` exposes a **message API**:
- **`send_message(&self, msg: &[u8])`** — sends one message (async). Message length is limited to 4 MiB. For **local** transports, the call waits up to **5 seconds** for the recipient to signal "data acked" (named event on Windows, named semaphore on Linux); if no ack is received, returns `SocketError::RecipientAckTimeout`. The recipient (e.g. `message_handler` or an external process) must signal the "data acked" event/semaphore after each successful read.
- **`message_handler(&self, callback)`** — invokes an async callback for each incoming message. The callback receives `Result<Vec<u8>, SocketError>`: `Ok(msg)` for each message, and `Err(e)` once when the read loop fails (e.g. connection lost), then the handler task stops.
- **`disconnect()`** — closes the connection (async). After calling it, `send_message()` and the `message_handler` callback will see a connection-lost error; the handler task then stops.
Example: send a message and handle incoming messages with a channel:
```rust
use ipcez::{socket_init, Socket};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let socket = socket_init("ipcez").await?;
let (tx, mut rx) = mpsc::unbounded_channel();
socket.message_handler(move |result| {
let _ = tx.send(result);
async {}
});
socket.send_message(b"hello").await?;
if let Some(Ok(msg)) = rx.recv().await {
println!("received {} bytes", msg.len());
}
Ok(())
}
```
### Connection loss (all transports)
For **local** connections (Unix domain socket and Windows named pipe), the library runs a liveness check (default 10 ms) on the read path. When the peer disconnects, **message_handler** receives one `Err(SocketError::Io(...))` (e.g. `ConnectionReset`, message `"connection lost"`) and then the handler task stops, so you get the **same kind of notification** as when a WebSocket connection is lost. You can also close the connection yourself by calling **`disconnect()`**; the same error behavior applies (e.g. one `Err(...)` to the callback, then the handler exits). **send_message** can also return an I/O error if the connection is already down. No configuration or code change is needed; this is under the hood.
### Local wire format (message framing)
For **local** connections, the wire format is **length-prefixed frames** so that one flush sends one message and read returns one message at a time (same semantics as WebSocket). Each frame is: 4-byte **big-endian** unsigned length (u32), then exactly that many bytes of payload. Messages larger than 4 MiB are rejected with an I/O error. Custom peers (e.g. a server that accepts the raw stream) must use the same frame format to interoperate.
### Socket errors
`socket_init` returns `Result<Socket, SocketError>`:
- **`SocketError::Io(e)`** — I/O failure (e.g. connection refused, pipe not found).
- **`SocketError::Ws(e)`** — WebSocket handshake or protocol error.
- **`SocketError::UnsupportedCombination(target, os)`** — That (target, os) pair is not supported on this platform (e.g. local + Linux when building on Windows).
- **`SocketError::Detection(e)`** — Environment variable detection failed (`os` or `target` unset or invalid); only when using `socket_init(endpoint)` with a non-URL endpoint.
- **`SocketError::RecipientAckTimeout`** — Local transport: the recipient did not signal "data acked" within 5 seconds after the sender signaled "data ready".
Once connected, **send_message** can return an I/O error when the connection is lost, or **RecipientAckTimeout** if the recipient never acknowledges; **message_handler** receives one `Err(...)` before the handler task exits, for any transport.
---
## Minimal end-to-end example
```rust
use ipcez::{socket_init, Socket};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set env: target=local, os=windows (or linux). Or use a ws:// URL for remote.
let socket = socket_init("ipcez").await?; // or "ws://localhost:8080/" for remote
let (tx, mut rx) = mpsc::unbounded_channel();
socket.message_handler(move |result| {
let _ = tx.send(result);
async {}
});
socket.send_message(b"hello").await?;
if let Some(Ok(msg)) = rx.recv().await {
println!("received {} bytes", msg.len());
}
Ok(())
}
```
---
## Language bindings
| Binding | Location | Requirements |
|-----------|--------------------|--------------|
| **Node/TS** | [wrappers/ts](wrappers/ts) | Node 18+, Rust (build addon) |
| **Python** | [wrappers/python](wrappers/python) | Python 3.9+, build `ipcez-ffi` cdylib |
| **C#** | [wrappers/csharp](wrappers/csharp) | .NET 8, build `ipcez-ffi` cdylib |
Build the C ABI library for Python and C# from the repo root: `cargo build -p ipcez-ffi [--release]`. Build the Node addon for TS: `cargo build -p ipcez-node [--release]`. See each wrapper’s README for install and usage.
---
## Build and test
```bash
cargo build
cargo test -- --test-threads=1
```
Use `--test-threads=1` so env-dependent tests do not race.
On Windows you can build and test everything (Rust, C#, Node addon, optional Python smoke) with:
```batch
build-and-test.bat
```
## API documentation
Generate and open the crate docs locally:
```bash
cargo doc --open
```
## License
MIT OR Apache-2.0