Sockets
A socket is a communication channel between two processes. Similar in concept to how WebSockets work — two endpoints sharing a buffer. One process writes into it, the other reads out of it.
The design mirrors Unix socket semantics deliberately. When KFS_7 adds the syscall
layer, sys_socket, sys_write, and sys_read will just be thin gates into these
same functions. No redesign needed.
The Table
#![allow(unused)]
fn main() {
const SOCKET_TABLE_SIZE: usize = 8;
pub static mut SOCKET_TABLE: [Option<*mut Socket>; SOCKET_TABLE_SIZE] = [None; SOCKET_TABLE_SIZE];
static mut SOCKET_COUNT: u32 = 0;
}
Same pattern as the process table — fixed array of optional pointers, a counter that
hands out unique IDs. SOCKET_TABLE_SIZE is a constant so the array size is
reusable without magic numbers.
The Struct
#![allow(unused)]
fn main() {
pub struct Socket {
id: u32,
sender: u32, // pid of the sending process
receiver: u32, // pid of the receiving process
buffer: *mut u8, // vmalloc'd data buffer
capacity: u32, // total buffer size
length: u32, // bytes currently in buffer
status: SocketStatus,
}
#[derive(PartialEq)]
enum SocketStatus {
Open,
Closed,
}
}
buffer is a pointer to a vmalloc’d region — capacity can vary per socket.
length tracks how much data is currently in the buffer so reads and writes know
where they are. The PartialEq derive on SocketStatus is needed so we can do
if (*s).status == SocketStatus::Open — Rust doesn’t give you == for free on
enums.
socket_create()
Allocates the socket struct on the heap, allocates the data buffer, initialises everything, and registers it in the table:
#![allow(unused)]
fn main() {
pub fn socket_create(sender: u32, receiver: u32, capacity: u32) -> i32 {
unsafe {
let ptr = memory::kmalloc(core::mem::size_of::<Socket>());
match ptr {
Some(p) => {
let socket = p as *mut Socket;
(*socket).id = SOCKET_COUNT;
(*socket).sender = sender;
(*socket).receiver = receiver;
(*socket).capacity = capacity;
(*socket).length = 0;
(*socket).status = SocketStatus::Open;
(*socket).buffer = memory::vmalloc(capacity as usize);
for i in 0..SOCKET_TABLE_SIZE {
if SOCKET_TABLE[i].is_none() {
SOCKET_TABLE[i] = Some(socket);
break;
}
}
SOCKET_COUNT += 1;
(*socket).id as i32
}
None => -1,
}
}
}
}
Returns the socket id on success, -1 on failure — Unix convention. The socket
struct goes on the heap via kmalloc, the data buffer goes in virtual memory via
vmalloc. Two separate allocations because they have different lifetimes and sizes.
socket_write()
Finds the socket by id, checks it’s open and has room, then copies data in using
memcpy from klib:
#![allow(unused)]
fn main() {
pub fn socket_write(id: u32, data: *const u8, len: u32) -> i32 {
unsafe {
for i in 0..SOCKET_TABLE_SIZE {
if let Some(s) = SOCKET_TABLE[i] {
if (*s).id == id {
if (*s).status == SocketStatus::Open {
if (*s).length + len <= (*s).capacity {
crate::klib::mem::memcpy(
(*s).buffer.add((*s).length as usize),
data as *mut u8,
len as usize,
);
(*s).length += len;
return len as i32;
}
return -1; // buffer full
}
return -1; // socket closed
}
}
}
-1 // socket not found
}
}
}
(*s).buffer.add((*s).length as usize) — writes start at the current end of the
buffered data, not at the beginning. This means multiple writes accumulate rather
than overwriting each other. The overflow check length + len <= capacity prevents
writing past the end of the allocation.
socket_read()
Copies from the buffer into the destination, then decrements length to mark the
data as consumed:
#![allow(unused)]
fn main() {
pub fn socket_read(id: u32, dest: *mut u8, len: u32) -> i32 {
unsafe {
for i in 0..SOCKET_TABLE_SIZE {
if let Some(s) = SOCKET_TABLE[i] {
if (*s).id == id {
if len <= (*s).length {
crate::klib::mem::memcpy(dest, (*s).buffer, len as usize);
(*s).length -= len;
return len as i32;
}
return -1; // not enough data available
}
}
}
-1 // socket not found
}
}
}
Note the check is len <= (*s).length — you can only read as much as is available.
Trying to read more than the buffer contains returns -1 rather than reading garbage.
socket_close()
Frees the data buffer, frees the socket struct, and clears the table entry:
#![allow(unused)]
fn main() {
pub fn socket_close(id: u32) {
unsafe {
for i in 0..SOCKET_TABLE_SIZE {
if let Some(s) = SOCKET_TABLE[i] {
if (*s).id == id {
memory::vfree((*s).buffer);
memory::kfree(s as *mut u8);
SOCKET_TABLE[i] = None;
}
}
}
}
}
}
Two frees — vfree for the vmalloc’d buffer, kfree for the kmalloc’d socket
struct. Order matters: free the contents before freeing the container.
Testing
> sys socket
=== Testing Sockets ===
Socket created: id=0
Write result: 20 bytes
Read result: 20 bytes
Message: hello from process 0
Socket closed
The test creates a socket between process 0 and process 1, writes a message, reads it back, verifies the contents, and closes the socket cleanly. Twenty bytes written, twenty bytes read, message intact.