//! Block-device client: the helper a filesystem uses to read and write a block //! device (a USB stick, via usb-storage) without hand-rolling the block-protocol //! IPC. Layered over `ipc` and the shared `block-protocol` wire format, like //! `runtime.usb` over the transfer protocol. //! //! Transfers name a caller-owned DMA buffer by physical address (from //! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size //! limit — the same handoff usb-storage uses toward the controller. const std = @import("std"); const envelope = @import("envelope"); const ipc = @import("ipc"); const block_protocol = @import("block-protocol"); const Protocol = block_protocol.Protocol; /// The medium_changed event payload, re-exported so a consumer decodes it without /// reaching into the wire-format module. pub const MediumChanged = block_protocol.MediumChanged; /// Decode a medium_changed event from a buffered-message payload a subscriber /// received (a `Received.isMessage` wake). Null if the bytes are too short to be /// one — a caller ignores anything that is not a well-formed event. pub fn decodeMediumChanged(payload: []const u8) ?MediumChanged { if (payload.len < envelope.prefix_size + @sizeOf(MediumChanged)) return null; return std.mem.bytesToValue(MediumChanged, payload[envelope.prefix_size..][0..@sizeOf(MediumChanged)]); } pub const Geometry = struct { block_size: u32, block_count: u64 }; pub const Device = struct { endpoint: ipc.Handle, /// The device's block size and total block count. pub fn geometry(self: Device) ?Geometry { var reply: [block_protocol.message_maximum]u8 = undefined; const answered = self.call(.geometry, {}, null, &reply) orelse return null; const result = Protocol.decodeReply(.geometry, answered) orelse return null; return .{ .block_size = result.block_size, .block_count = result.block_count }; } /// Hand the block server a DMA-region capability (`handle` — from a `shareable` /// dma_alloc) so it forwards it to the controller and the buffer's physical /// addresses become reachable by the device. Call once per buffer before naming it /// in `read`/`write`. Harmless success when no IOMMU is enforcing. pub fn attach(self: Device, handle: ipc.Handle) bool { var reply: [block_protocol.message_maximum]u8 = undefined; return self.call(.attach, {}, handle, &reply) != null; } /// The reverse of `attach`: the buffer leaves the device's reach. The same /// region capability rides again (the kernel matches the region). Do not name /// the buffer's physical address in `read`/`write` after this. pub fn detach(self: Device, handle: ipc.Handle) bool { var reply: [block_protocol.message_maximum]u8 = undefined; return self.call(.detach, {}, handle, &reply) != null; } /// Read `count` blocks starting at `lba` into the DMA buffer at `physical`. pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool { var reply: [block_protocol.message_maximum]u8 = undefined; return self.call(.read, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null; } /// Write `count` blocks starting at `lba` from the DMA buffer at `physical`. pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool { var reply: [block_protocol.message_maximum]u8 = undefined; return self.call(.write, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null; } /// Commit any device write cache to stable media (SCSI SYNCHRONIZE CACHE), so /// prior writes survive a power-off. A filesystem calls this before the machine /// goes down; no data transfer, so the buffer arguments are unused. pub fn flush(self: Device) bool { var reply: [block_protocol.message_maximum]u8 = undefined; return self.call(.flush, {}, null, &reply) != null; } /// Confine the process `badge` to blocks `[base_lba, base_lba + block_count)` /// on this device — the volume manager's per-volume grant to a filesystem. /// The caller must itself be unconfined (whole-device); a confined caller is /// refused, so a filesystem cannot widen its own range. See `DefineRange`. pub fn defineRange(self: Device, badge: u32, base_lba: u64, block_count: u64) bool { var reply: [block_protocol.message_maximum]u8 = undefined; return self.call(.define_range, .{ .badge = badge, .base_lba = base_lba, .block_count = block_count }, null, &reply) != null; } /// One request at the driver. `target` is always 0: one endpoint per device, so /// there is no object within the peer to address. fn call( self: Device, comptime operation: Protocol.Operation, request: Protocol.RequestOf(operation), capability: ?ipc.Handle, reply: []u8, ) ?[]u8 { var packet: [block_protocol.message_maximum]u8 = undefined; const framed = Protocol.encodeRequest(operation, 0, request, &.{}, &packet) orelse return null; const answer = ipc.callCap(self.endpoint, framed, reply, capability) catch return null; const status = envelope.statusOf(reply[0..answer.len]) orelse return null; if (status.status != 0) return null; return reply[0..answer.len]; } /// Subscribe `subscriber` (an endpoint) to this device's medium_changed /// events: the reserved `subscribe` verb carries the subscriber's endpoint as /// the capability, and the driver then ipc.sends each medium transition to it. pub fn subscribeMedium(self: Device, subscriber: ipc.Handle) bool { var packet: [block_protocol.message_maximum]u8 = undefined; const framed = envelope.encodeSubscribe(0, &packet) orelse return false; // interest 0: every event (block has one) var reply: [block_protocol.message_maximum]u8 = undefined; const answer = ipc.callCap(self.endpoint, framed, &reply, subscriber) catch return false; const status = envelope.statusOf(reply[0..answer.len]) orelse return false; return status.status == 0; } /// Unsubscribe from this device's medium_changed events. Call before closing /// the channel so the driver's bounded subscriber table frees the slot rather /// than holding a dead endpoint until an exit sweep notices. pub fn unsubscribeMedium(self: Device) bool { var packet: [block_protocol.message_maximum]u8 = undefined; const framed = envelope.encodeUnsubscribe(&packet) orelse return false; var reply: [block_protocol.message_maximum]u8 = undefined; const answer = ipc.callCap(self.endpoint, framed, &reply, null) catch return false; const status = envelope.statusOf(reply[0..answer.len]) orelse return false; return status.status == 0; } }; // There is deliberately no open-by-name here: `block` is not a registry name. // One storage process serves each volume, and a consumer receives its volume's // channel from the device manager (establishment by lineage, communication.md // "Establishment: two planes"), then wraps it: `block.Device{ .endpoint = c }`.