# I/O API Layers and Selection
Spargio exposes layered I/O APIs. Choose the highest layer that fits your use case.
## Layer 1: Ergonomic fs/net APIs
High-level APIs:
- `spargio::fs::{OpenOptions, File}` plus path helpers (`create_dir*`, `rename`, `remove_*`, metadata/link helpers, `read`, `write`).
- `spargio::net::{TcpListener, TcpStream, UdpSocket, UnixListener, UnixStream, UnixDatagram}`.
- `spargio::io::{AsyncRead, AsyncWrite, split, copy_to_vec, BufReader, BufWriter}` and framed helpers.
Use this layer first for most services.
```rust
#[spargio::main]
async fn main(handle: spargio::RuntimeHandle) -> std::io::Result<()> {
spargio::fs::write(&handle, "/tmp/spargio-io-layer1.txt", b"hello").await?;
let bytes = spargio::fs::read(&handle, "/tmp/spargio-io-layer1.txt").await?;
assert_eq!(&bytes, b"hello");
let listener = spargio::net::TcpListener::bind(handle.clone(), "127.0.0.1:7002").await?;
let (stream, _) = listener.accept_round_robin().await?;
let payload = stream.recv(4096).await?;
stream.write_all(&payload).await?;
Ok(())
}
```
What this does:
- writes and reads a file through the high-level fs helpers.
- accepts one TCP connection through the high-level net helper.
- reads a payload and echoes it back with stream helpers.
```rust
use spargio::io::{AsyncWrite, BufReader, BufWriter, copy_to_vec, split};
async fn copy_and_ack(stream: spargio::net::TcpStream) -> std::io::Result<Vec<u8>> {
let (reader, writer) = split(stream);
let reader = BufReader::with_capacity(reader, 16 * 1024);
let mut all_bytes = Vec::new();
let _copied = copy_to_vec(&reader, &mut all_bytes, 8 * 1024).await?;
let buffered = BufWriter::new(writer);
let _ = buffered.write_owned(b"ok\n".to_vec()).await?;
buffered.flush().await?;
Ok(all_bytes)
}
```
What this does:
- splits one stream into read/write halves with `split`.
- buffers reads with `BufReader` and copies bytes with `copy_to_vec`.
- writes a buffered acknowledgement via `BufWriter`.
## Layer 2: Native unbound API
For direct operation control and shard-aware routing controls:
- `RuntimeHandle::uring_native_unbound() -> UringNativeAny`
- `UringNativeAny::with_preferred_shard(...)`
- `UringNativeAny::select_shard(...)`
This is appropriate for advanced pipelines and extension authors.
What "unbound" means here:
- the API handle is not permanently tied to one shard
- each submitted operation is routed at submission time by Spargio's selector
- you can provide routing hints (preferred shard), but routing is still per-op
This differs from session-bound stream APIs (for example a `TcpStream` session shard),
where operations are intentionally kept on that stream's session shard.
```rust
#[cfg(all(feature = "uring-native", target_os = "linux"))]
#[spargio::main]
async fn main(handle: spargio::RuntimeHandle) -> std::io::Result<()> {
use std::os::fd::AsRawFd;
let native_any = handle
.uring_native_unbound()
.expect("io_uring backend required");
let preferred = 0 as spargio::ShardId;
// Configure a default preferred shard hint on this unbound handle.
let native = native_any
.with_preferred_shard(preferred)
.expect("valid preferred shard");
// `None` means "no per-call override": use the handle's configured preferred
// shard hint (if any), then let the selector make the final routing choice.
let selected = native.select_shard(None).expect("select shard");
println!("native preferred_shard={preferred} selected_shard={selected}");
std::fs::write("/tmp/spargio-native-any.txt", b"native")?;
let file = std::fs::OpenOptions::new()
.read(true)
.open("/tmp/spargio-native-any.txt")?;
let out = native.read_at(file.as_raw_fd(), 0, 6).await?;
assert_eq!(&out, b"native");
Ok(())
}
```
What this does:
- acquires `UringNativeAny` from the runtime handle.
- sets an explicit preferred shard for native routing.
- lets you inspect the selector's chosen shard before submitting operations.
- submits a direct `read_at` against a raw fd.
- gets completion as a regular async result without helper threads.
`with_preferred_shard(...)` is a preferred-routing control. For strict per-op
shard routing, use `submit_unsafe_on_shard(...)` in Layer 3.
## Layer 3: Custom io_uring Opcodes via Unsafe Extension API
For custom SQE/CQE workflows not in core APIs:
- `UringNativeAny::submit_unsafe`
- `UringNativeAny::submit_unsafe_on_shard`
Use safe wrappers around unsafe calls. See [Extending Spargio with Custom io_uring Opcodes](11_native_extensions.md).
```rust
#[cfg(all(feature = "uring-native", target_os = "linux"))]
#[spargio::main]
async fn main(handle: spargio::RuntimeHandle) -> std::io::Result<()> {
use io_uring::opcode;
let native = handle
.uring_native_unbound()
.expect("io_uring backend required");
let shard = native.select_shard(Some(0)).expect("select shard");
let cqe_result = unsafe {
native
.submit_unsafe_on_shard(
shard,
(),
|_| Ok(opcode::Nop::new().build()),
|(), cqe| Ok::<i32, std::io::Error>(cqe.result),
)
.await?
};
assert_eq!(cqe_result, 0);
Ok(())
}
```
What this does:
- selects an explicit shard for this low-level operation.
- submits a custom SQE (`NOP`) not wrapped by a high-level helper.
- decodes the CQE in a completion closure.
- keeps unsafe isolated to one call site, which is the expected extension pattern.
## DNS Caveat
Hostname `ToSocketAddrs` paths can still block for resolution. For strict non-DNS data-plane behavior, use explicit `SocketAddr` APIs:
- `connect_socket_addr*`
- `bind_socket_addr`
```rust
#[spargio::main]
async fn main(handle: spargio::RuntimeHandle) -> std::io::Result<()> {
let addr: std::net::SocketAddr = "127.0.0.1:8080".parse().expect("socket addr");
let _stream = spargio::net::TcpStream::connect_socket_addr(handle, addr).await?;
Ok(())
}
```
What this does: bypasses hostname lookup entirely and connects on a concrete `SocketAddr`.
## Filesystem Caveat
Some fs helpers are still compatibility blocking paths (`canonicalize`, `metadata`, `symlink_metadata`, `set_permissions`) while higher-value helpers are native-first.
## Directory Traversal and DU
Available now:
- `spargio::fs::read_dir(...)`
- `spargio::fs::du(...)` with `DuOptions` and `DuSummary`
- low-level `spargio::extension::fs::read_dir_entries(...)`
```rust
#[spargio::main]
async fn main(handle: spargio::RuntimeHandle) -> std::io::Result<()> {
let entries = spargio::fs::read_dir(&handle, "/var/log").await?;
let du = spargio::fs::du(&handle, "/var/log", spargio::fs::DuOptions::default()).await?;
let raw = spargio::extension::fs::read_dir_entries(handle.clone(), "/var/log").await?;
println!(
"entries={} raw_entries={} total_bytes={}",
entries.len(),
raw.len(),
du.total_bytes
);
Ok(())
}
```
What this does:
- uses high-level `read_dir` for ergonomic traversal.
- uses `du` to summarize recursive disk usage with policy knobs.
- uses low-level `read_dir_entries` when you need raw extension-layer entry data.
Current limitation (as of 2026-03-03): fully in-ring directory traversal via `IORING_OP_GETDENTS` is not yet available in upstream stable ABI surfaces, so traversal currently uses a compatibility helper lane.