Skip to content

Implementing a Host

github-actions[bot] edited this page Aug 27, 2026 · 1 revision

Implementing a Host

typebit::Host is the only interface you implement. It has ~18 methods, all no_std-friendly. The engine calls them from tick() on your thread, so they must not block for long (use non-blocking sockets).

The trait

pub trait Host {
    fn now_ms(&self) -> u64;                        // clock (ms)
    fn fill_random(&mut self, buf: &mut [u8]);      // CSPRNG
    fn log(&mut self, level: LogLevel, msg: &str);

    // HTTP(S) GET → append body to `out` (trackers, web seeds, IPFS gateways)
    fn http_get(&mut self, url: &str, timeout_ms: u64, out: &mut Vec<u8>)
        -> Result<()>;

    // DNS: resolve a hostname for the DHT bootstrap routers (BEP-5).
    // Optional — returning None disables DHT bootstrap (HTTP/UDP trackers
    // still work). The FFI `HostCbs.resolve_host` is the C twin of this.
    fn resolve_host(&self, host: &str, port: u16) -> Option<NetAddr>;

    // TCP (non-blocking)
    fn tcp_connect(&mut self, addr: &NetAddr) -> Result<ConnId>;
    fn tcp_connect_done(&mut self, id: ConnId) -> Result<()>;  // Ok=established
    fn tcp_send(&mut self, id: ConnId, data: &[u8]) -> Result<usize>;
    fn tcp_recv(&mut self, id: ConnId, buf: &mut [u8]) -> Result<usize>;
    fn tcp_close(&mut self, id: ConnId);

    // UDP (DHT, UDP trackers)
    fn udp_open(&mut self, port: u16) -> Result<()>;
    fn udp_send(&mut self, addr: &NetAddr, data: &[u8]) -> Result<()>;
    fn udp_recv(&mut self, buf: &mut [u8]) -> Result<(NetAddr, usize)>;

    // Files (piece data)
    fn disk_open(&mut self, path: &str) -> Result<DiskId>;
    fn disk_read(&mut self, id: DiskId, offset: u64, buf: &mut [u8]) -> Result<usize>;
    fn disk_write(&mut self, id: DiskId, offset: u64, data: &[u8]) -> Result<()>;
    fn disk_prealloc(&mut self, id: DiskId, size: u64) -> Result<()>;
    fn disk_flush(&mut self, id: DiskId) -> Result<()>;
    fn disk_close(&mut self, id: DiskId);
}

Conventions the engine relies on

  • tcp_recv / udp_recv return Err(Error::WouldBlock) when there is no data right now. Do not block in these.
  • tcp_connect returns a handle immediately (non-blocking connect); the engine polls with tcp_connect_done.
  • http_get may block up to timeout_ms; the engine uses it for trackers and web seeds.
  • Disk offsets are absolute within one file (per-file handles).
  • UDP is optional, never fatal. The engine opens the UDP socket lazily and only when something needs it (DHT enabled, a UDP tracker, or port mapping). If udp_open fails, start() still succeeds: DHT and UDP trackers are disabled, an EngineEvent::Error { code: 0 } is emitted, and HTTP trackers + peer transport keep working. To actually get DHT working, implement resolve_host and real udp_open/udp_send/ udp_recv.

A real (std) host in ~60 lines

use std::net::TcpStream;
use typebit::platform::{ConnId, DiskId, Host, LogLevel, NetAddr};
use typebit::{Error, Result};

pub struct StdHost {
    tcp: HashMap<ConnId, TcpStream>,
    disks: HashMap<DiskId, std::fs::File>,
    next_id: u32,
}

impl Host for StdHost {
    fn now_ms(&self) -> u64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as u64
    }
    fn fill_random(&mut self, buf: &mut [u8]) {
        // os random (e.g. getrandom or /dev/urandom)
    }
    fn tcp_connect(&mut self, addr: &NetAddr) -> Result<ConnId> {
        let (ip, port) = match addr {
            NetAddr::V4(ip, p) => (std::net::Ipv4Addr::from(*ip), *p),
            NetAddr::V6(ip, p) => (std::net::Ipv6Addr::from(*ip), *p),
        };
        let stream = TcpStream::connect((ip, port)).map_err(|_| Error::Io)?;
        stream.set_nonblocking(true).map_err(|_| Error::Io)?;
        let id = self.next_id;
        self.next_id += 1;
        self.tcp.insert(id, stream);
        Ok(id)
    }
    fn tcp_recv(&mut self, id: ConnId, buf: &mut [u8]) -> Result<usize> {
        use std::io::Read;
        match self.tcp.get_mut(&id).unwrap().read(buf) {
            Ok(n) => Ok(n),
            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Err(Error::WouldBlock),
            Err(_) => Err(Error::Io),
        }
    }
    // ... and the rest. See examples/minimal_host.rs for a complete mock.
}

Optional capabilities (all have defaults)

Method Default Needed for
resolve_host(host, port) -> Option<NetAddr> None DHT bootstrap
resolve_host_all(host, port) -> Vec<NetAddr> resolve_host only UDP tracker multi-address fallback
resolve_host_async + take_resolved_hosts() false / empty non-blocking DHT bootstrap DNS
http_post(url, body, timeout, out) Err(NotSupported) UPnP IGD SOAP
http_get_range(url, start, end, timeout, out) Err(NotSupported) web seeds (BEP-19)
http_get_async(url, timeout) -> u64 + http_take_done() 0 / empty non-blocking tracker / web seed
udp_multicast_send / udp_join_multicast udp_send / no-op LSD (BEP-14), SSDP
default_gateway() -> Option<NetAddr> None NAT-PMP
local_ip() -> Option<NetAddr> None UPnP IGD NewInternalClient
tcp_recv_buf_size() -> usize 64 KiB receive-buffer sizing hint

Note: keep the out buffers of every HTTP callback bounded. The engine enforces caps (MAX_HTTP_BODY / request window), but a host implementation should also defend against hostile response bodies.

Integration checklist

  1. Engine::new(host, config) — one per app
  2. add_torrent(&bytes, save_dir) or add_magnet(uri, save_dir) → infohash
  3. start(&hash)
  4. on a timer: tick(), then take_events() and forward events to your UI
  5. inbound sockets: on_inbound_connection(conn, addr)
  6. persistence: save_state() / load_state(&state, now)

That's the whole integration surface. Everything else is internal.

Clone this wiki locally