From c5aa79a100d6bb286d0b253ad345005b2ca8569a Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Sun, 28 Dec 2025 18:32:44 -0800 Subject: [PATCH 1/7] Add TCP listener capability and runtime support --- capc/src/codegen/intrinsics.rs | 58 ++++++++++++++++++++++++++++++++++ runtime/src/lib.rs | 58 +++++++++++++++++++++++++++++++++- stdlib/sys/net.cap | 15 +++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/capc/src/codegen/intrinsics.rs b/capc/src/codegen/intrinsics.rs index ef80c44..f33b5e3 100644 --- a/capc/src/codegen/intrinsics.rs +++ b/capc/src/codegen/intrinsics.rs @@ -172,6 +172,19 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { ret: AbiType::Handle, }; // Net. + let net_listen = FnSig { + params: vec![AbiType::Handle, AbiType::String, AbiType::I32], + ret: AbiType::Result(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + }; + let net_listen_abi = FnSig { + params: vec![ + AbiType::Handle, + AbiType::String, + AbiType::I32, + AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + ], + ret: AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + }; let net_connect = FnSig { params: vec![AbiType::Handle, AbiType::String, AbiType::I32], ret: AbiType::Result(Box::new(AbiType::Handle), Box::new(AbiType::I32)), @@ -205,10 +218,25 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { ], ret: AbiType::ResultOut(Box::new(AbiType::Unit), Box::new(AbiType::I32)), }; + let net_accept = FnSig { + params: vec![AbiType::Handle], + ret: AbiType::Result(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + }; + let net_accept_abi = FnSig { + params: vec![ + AbiType::Handle, + AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + ], + ret: AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + }; let net_close = FnSig { params: vec![AbiType::Handle], ret: AbiType::Unit, }; + let net_listener_close = FnSig { + params: vec![AbiType::Handle], + ret: AbiType::Unit, + }; let args_at = FnSig { params: vec![AbiType::Handle, AbiType::I32], ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), @@ -649,6 +677,16 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { }, ); // === Net === + map.insert( + "sys.net.Net__listen".to_string(), + FnInfo { + sig: net_listen, + abi_sig: Some(net_listen_abi), + symbol: "capable_rt_net_listen".to_string(), + runtime_symbol: None, + is_runtime: true, + }, + ); map.insert( "sys.net.Net__connect".to_string(), FnInfo { @@ -659,6 +697,26 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { is_runtime: true, }, ); + map.insert( + "sys.net.TcpListener__accept".to_string(), + FnInfo { + sig: net_accept, + abi_sig: Some(net_accept_abi), + symbol: "capable_rt_net_accept".to_string(), + runtime_symbol: None, + is_runtime: true, + }, + ); + map.insert( + "sys.net.TcpListener__close".to_string(), + FnInfo { + sig: net_listener_close, + abi_sig: None, + symbol: "capable_rt_net_listener_close".to_string(), + runtime_symbol: None, + is_runtime: true, + }, + ); map.insert( "sys.net.TcpConn__read_to_string".to_string(), FnInfo { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 319d5f2..d66a5b6 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::ffi::OsStr; use std::io::{self, Read, Write}; -use std::net::TcpStream; +use std::net::{TcpListener, TcpStream}; use std::path::{Component, Path, PathBuf}; use std::sync::{LazyLock, Mutex}; @@ -28,6 +28,8 @@ static STDIN_CAPS: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); static NET_CAPS: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); +static TCP_LISTENERS: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); static TCP_CONNS: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); static SLICES: LazyLock>> = @@ -772,6 +774,55 @@ pub extern "C" fn capable_rt_net_connect( } } +#[no_mangle] +pub extern "C" fn capable_rt_net_listen( + net: Handle, + host_ptr: *const u8, + host_len: usize, + port: i32, + out_ok: *mut Handle, + out_err: *mut i32, +) -> u8 { + if !has_handle(&NET_CAPS, net, "net table") { + return write_handle_result_code(out_ok, out_err, Err(NetErr::IoError as i32)); + } + let host = unsafe { read_str(host_ptr, host_len) }; + let Some(host) = host else { + return write_handle_result_code(out_ok, out_err, Err(NetErr::InvalidAddress as i32)); + }; + if host.is_empty() || port <= 0 || port > u16::MAX as i32 { + return write_handle_result_code(out_ok, out_err, Err(NetErr::InvalidAddress as i32)); + } + match TcpListener::bind((host.as_str(), port as u16)) { + Ok(listener) => { + let handle = new_handle(); + insert_handle(&TCP_LISTENERS, handle, listener, "tcp listener table"); + write_handle_result_code(out_ok, out_err, Ok(handle)) + } + Err(err) => write_handle_result_code(out_ok, out_err, Err(map_net_err(err) as i32)), + } +} + +#[no_mangle] +pub extern "C" fn capable_rt_net_accept( + listener: Handle, + out_ok: *mut Handle, + out_err: *mut i32, +) -> u8 { + let listener = take_handle(&TCP_LISTENERS, listener, "tcp listener table"); + let Some(listener) = listener else { + return write_handle_result_code(out_ok, out_err, Err(NetErr::IoError as i32)); + }; + match listener.accept() { + Ok((stream, _addr)) => { + let handle = new_handle(); + insert_handle(&TCP_CONNS, handle, stream, "tcp conn table"); + write_handle_result_code(out_ok, out_err, Ok(handle)) + } + Err(err) => write_handle_result_code(out_ok, out_err, Err(map_net_err(err) as i32)), + } +} + #[no_mangle] pub extern "C" fn capable_rt_net_read_to_string( conn: Handle, @@ -844,6 +895,11 @@ pub extern "C" fn capable_rt_net_close(conn: Handle) { take_handle(&TCP_CONNS, conn, "tcp conn table"); } +#[no_mangle] +pub extern "C" fn capable_rt_net_listener_close(listener: Handle) { + take_handle(&TCP_LISTENERS, listener, "tcp listener table"); +} + #[no_mangle] pub extern "C" fn capable_rt_start() -> i32 { let sys = new_handle(); diff --git a/stdlib/sys/net.cap b/stdlib/sys/net.cap index a34f95f..0403b9a 100644 --- a/stdlib/sys/net.cap +++ b/stdlib/sys/net.cap @@ -2,6 +2,7 @@ package unsafe module sys::net pub copy capability struct Net +pub linear capability struct TcpListener pub linear capability struct TcpConn pub enum NetErr { @@ -11,11 +12,25 @@ pub enum NetErr { } impl Net { + pub fn listen(self, host: string, port: i32) -> Result[TcpListener, NetErr] { + return Err(NetErr::IoError) + } + pub fn connect(self, host: string, port: i32) -> Result[TcpConn, NetErr] { return Err(NetErr::IoError) } } +impl TcpListener { + pub fn accept(self) -> Result[TcpConn, NetErr] { + return Err(NetErr::IoError) + } + + pub fn close(self) -> unit { + return () + } +} + impl TcpConn { pub fn read_to_string(self: &TcpConn) -> Result[string, NetErr] { return Err(NetErr::IoError) From 16e7394827bbc7d1f445068c04798b699853e625 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Sun, 28 Dec 2025 18:32:51 -0800 Subject: [PATCH 2/7] Add HTTP server example using net caps --- examples/http_server/http_server.cap | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 examples/http_server/http_server.cap diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap new file mode 100644 index 0000000..e040443 --- /dev/null +++ b/examples/http_server/http_server.cap @@ -0,0 +1,32 @@ +package unsafe +module http_server +use sys::console +use sys::net +use sys::system + +fn serve_once(c: Console, net: Net) -> Result[unit, NetErr] { + let listener = net.listen("127.0.0.1", 8080)? + let conn = listener.accept()? + let req = conn.read_to_string()? + c.println("request:") + c.println(req) + let response = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\n\r\nhello world\n" + conn.write(response)? + conn.close() + return Ok(()) +} + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + let net = rc.mint_net() + c.println("listening on 127.0.0.1:8080") + let res = serve_once(c, net) + match (res) { + Ok(_) => { + } + Err(_) => { + c.println("server error") + } + } + return 0 +} From 9c9bf35a52e5dc323bf4c265b5922bd98c856820 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Sun, 28 Dec 2025 18:33:03 -0800 Subject: [PATCH 3/7] Add justfile helper for HTTP server example --- justfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/justfile b/justfile index b66ac47..04f7ed4 100644 --- a/justfile +++ b/justfile @@ -48,5 +48,8 @@ extern-demo-build: extern-demo-run: cargo run -p capc -- run --link-search examples/extern_demo --link-lib extern_demo examples/extern_demo/extern_demo.cap +http-server: + cargo run -p capc -- run examples/http_server/http_server.cap + lsp: cargo run -p caplsp From a864b150fc2ad2bd9d4154b7fdd2c98cdfb23efe Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Sun, 28 Dec 2025 18:33:36 -0800 Subject: [PATCH 4/7] Mark HTTP server example as safe --- examples/http_server/http_server.cap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index e040443..fb52283 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -1,4 +1,4 @@ -package unsafe +package safe module http_server use sys::console use sys::net From 9dacbf397b854c90c8041f88dd25a34e7647dbfb Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Sun, 28 Dec 2025 18:38:28 -0800 Subject: [PATCH 5/7] Expand HTTP example into static file server --- examples/http_server/http_server.cap | 149 +++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 6 deletions(-) diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index fb52283..b73200a 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -1,17 +1,151 @@ package safe module http_server use sys::console +use sys::fs use sys::net +use sys::args use sys::system -fn serve_once(c: Console, net: Net) -> Result[unit, NetErr] { +fn arg_or_default(args: Args, index: i32, default: string) -> string { + if (args.len() > index) { + let res = args.at(index) + match (res) { + Ok(value) => { + return value + } + Err(_) => { + } + } + } + return default +} + +fn strip_query(raw_path: string) -> string { + let parts = raw_path.split(63u8) + let res = parts.get(0) + match (res) { + Ok(path) => { + return path + } + Err(_) => { + } + } + return "" +} + +fn sanitize_path(raw_path: string) -> Result[string, unit] { + let parts = raw_path.split(47u8) + let out = "" + let i = 0 + while i < parts.len() { + let seg_res = parts.get(i) + match (seg_res) { + Ok(seg) => { + if (seg.len() == 0) { + } else { + if (seg == ".") { + } else { + if (seg == "..") { + return Err(()) + } else { + if (out.len() == 0) { + out = seg + } else { + out = fs::join(out, seg) + } + } + } + } + } + Err(_) => { + return Err(()) + } + } + i = i + 1 + } + if (out.len() == 0) { + return Ok("index.html") + } + return Ok(out) +} + +fn parse_request_path(req: string) -> Result[string, unit] { + let lines = req.lines() + let line_res = lines.get(0) + let line = "" + match (line_res) { + Ok(value) => { + line = value.trim() + } + Err(_) => { + return Err(()) + } + } + let parts = line.split(32u8) + let method_res = parts.get(0) + let method = "" + match (method_res) { + Ok(value) => { + method = value + } + Err(_) => { + return Err(()) + } + } + if (method != "GET") { + return Err(()) + } + let path_res = parts.get(1) + let raw_path = "" + match (path_res) { + Ok(value) => { + raw_path = value + } + Err(_) => { + return Err(()) + } + } + let cleaned = strip_query(raw_path) + return sanitize_path(cleaned) +} + +fn respond_ok(conn: &TcpConn, body: string) -> Result[unit, NetErr] { + conn.write("HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n")? + conn.write(body)? + return Ok(()) +} + +fn respond_not_found(conn: &TcpConn) -> Result[unit, NetErr] { + conn.write("HTTP/1.0 404 Not Found\r\nContent-Type: text/plain\r\n\r\nnot found\n")? + return Ok(()) +} + +fn respond_bad_request(conn: &TcpConn) -> Result[unit, NetErr] { + conn.write("HTTP/1.0 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nbad request\n")? + return Ok(()) +} + +fn serve_once(c: Console, net: Net, readfs: ReadFS) -> Result[unit, NetErr] { let listener = net.listen("127.0.0.1", 8080)? let conn = listener.accept()? let req = conn.read_to_string()? - c.println("request:") - c.println(req) - let response = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\n\r\nhello world\n" - conn.write(response)? + let path_res = parse_request_path(req) + match (path_res) { + Ok(path) => { + let file_res = readfs.read_to_string(path) + match (file_res) { + Ok(body) => { + respond_ok(conn, body)? + } + Err(_) => { + respond_not_found(conn)? + } + } + } + Err(_) => { + respond_bad_request(conn)? + } + } conn.close() return Ok(()) } @@ -19,8 +153,11 @@ fn serve_once(c: Console, net: Net) -> Result[unit, NetErr] { pub fn main(rc: RootCap) -> i32 { let c = rc.mint_console() let net = rc.mint_net() + let args = rc.mint_args() + let root = arg_or_default(args, 1, ".") + let readfs = rc.mint_readfs(root) c.println("listening on 127.0.0.1:8080") - let res = serve_once(c, net) + let res = serve_once(c, net, readfs) match (res) { Ok(_) => { } From be666d9874cf1a3cef482b4b5fc4fb88b019a19a Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Sun, 28 Dec 2025 18:39:39 -0800 Subject: [PATCH 6/7] Add string equality helper for HTTP example --- examples/http_server/http_server.cap | 14 +++++++------- stdlib/sys/string.cap | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index b73200a..c3b155a 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -43,14 +43,14 @@ fn sanitize_path(raw_path: string) -> Result[string, unit] { Ok(seg) => { if (seg.len() == 0) { } else { - if (seg == ".") { + if (seg.eq(".")) { + } else { + if (seg.eq("..")) { + return Err(()) } else { - if (seg == "..") { - return Err(()) + if (out.len() == 0) { + out = seg } else { - if (out.len() == 0) { - out = seg - } else { out = fs::join(out, seg) } } @@ -92,7 +92,7 @@ fn parse_request_path(req: string) -> Result[string, unit] { return Err(()) } } - if (method != "GET") { + if (!method.eq("GET")) { return Err(()) } let path_res = parts.get(1) diff --git a/stdlib/sys/string.cap b/stdlib/sys/string.cap index 3b3c99e..e5f9c3e 100644 --- a/stdlib/sys/string.cap +++ b/stdlib/sys/string.cap @@ -108,4 +108,20 @@ impl string { } return self.byte_at(len - 1) == suffix } + + pub fn eq(self, other: string) -> bool { + let self_len = self.len() + let other_len = other.len() + if (self_len != other_len) { + return false + } + let i = 0 + while (i < self_len) { + if self.byte_at(i) != other.byte_at(i) { + return false + } + i = i + 1 + } + return true + } } From 2213bee022d79a3e483f77bce80ff753c5c891c4 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Sun, 28 Dec 2025 18:48:57 -0800 Subject: [PATCH 7/7] wip http --- examples/http_server/http_server.cap | 121 +++++++++++++++------------ 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index c3b155a..7b876f3 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -1,3 +1,5 @@ +// WIP! This is a sample serving to improve the language. + package safe module http_server use sys::console @@ -23,90 +25,101 @@ fn arg_or_default(args: Args, index: i32, default: string) -> string { fn strip_query(raw_path: string) -> string { let parts = raw_path.split(63u8) let res = parts.get(0) - match (res) { + return match (res) { Ok(path) => { - return path + path } Err(_) => { + "" } } - return "" } -fn sanitize_path(raw_path: string) -> Result[string, unit] { - let parts = raw_path.split(47u8) - let out = "" - let i = 0 - while i < parts.len() { - let seg_res = parts.get(i) - match (seg_res) { - Ok(seg) => { - if (seg.len() == 0) { - } else { - if (seg.eq(".")) { - } else { - if (seg.eq("..")) { - return Err(()) - } else { - if (out.len() == 0) { - out = seg - } else { - out = fs::join(out, seg) - } - } - } - } - } - Err(_) => { - return Err(()) - } - } - i = i + 1 +fn sanitize_segment(parts: VecString, i: i32, acc: string, seg: string) -> Result[string, unit] { + if (seg.len() == 0) { + return sanitize_parts(parts, i + 1, acc) + } + if (seg.eq(".")) { + return sanitize_parts(parts, i + 1, acc) } - if (out.len() == 0) { - return Ok("index.html") + if (seg.eq("..")) { + return Err(()) + } + if (acc.len() == 0) { + return sanitize_parts(parts, i + 1, seg) } - return Ok(out) + let next = fs::join(acc, seg) + return sanitize_parts(parts, i + 1, next) } -fn parse_request_path(req: string) -> Result[string, unit] { - let lines = req.lines() - let line_res = lines.get(0) - let line = "" - match (line_res) { - Ok(value) => { - line = value.trim() +fn sanitize_parts(parts: VecString, i: i32, acc: string) -> Result[string, unit] { + if (i >= parts.len()) { + return Ok(acc) + } + let seg_res = parts.get(i) + return match (seg_res) { + Ok(seg) => { + sanitize_segment(parts, i, acc, seg) + } + Err(_) => { + Err(()) + } + } +} + +fn sanitize_path(raw_path: string) -> Result[string, unit] { + let parts = raw_path.split(47u8) + let res = sanitize_parts(parts, 0, "") + match (res) { + Ok(path) => { + if (path.len() == 0) { + return Ok("index.html") + } + return Ok(path) } Err(_) => { return Err(()) } } - let parts = line.split(32u8) +} + +fn parse_request_line(line: string) -> Result[string, unit] { + let trimmed = line.trim() + let parts = trimmed.split(32u8) let method_res = parts.get(0) - let method = "" match (method_res) { - Ok(value) => { - method = value + Ok(method) => { + if (!method.eq("GET")) { + return Err(()) + } } Err(_) => { return Err(()) } } - if (!method.eq("GET")) { - return Err(()) - } let path_res = parts.get(1) - let raw_path = "" match (path_res) { - Ok(value) => { - raw_path = value + Ok(raw_path) => { + let cleaned = strip_query(raw_path) + return sanitize_path(cleaned) } Err(_) => { return Err(()) } } - let cleaned = strip_query(raw_path) - return sanitize_path(cleaned) +} + +fn parse_request_path(req: string) -> Result[string, unit] { + let lines = req.lines() + let line_res = lines.get(0) + return match (line_res) { + Ok(line) => { + parse_request_line(line) + } + Err(_) => { + Err(()) + } + } } fn respond_ok(conn: &TcpConn, body: string) -> Result[unit, NetErr] {