From bad32fdef2c36e23d4fe175baeb0e4a55be8be01 Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 21 Nov 2025 20:17:00 +0100 Subject: [PATCH] Initial networking module. --- NETWORKING.md | 283 +++++++++++++++++++++++ examples/http-server-simple/main.orc | 29 +++ examples/http-server/src/main.orc | 31 +++ examples/tcp-client/src/main.orc | 13 ++ examples/tcp-echo-server/src/main.orc | 19 ++ orchid-std/src/std/mod.rs | 1 + orchid-std/src/std/socket/mod.rs | 2 + orchid-std/src/std/socket/socket_atom.rs | 110 +++++++++ orchid-std/src/std/socket/socket_lib.rs | 131 +++++++++++ orchid-std/src/std/std_system.rs | 5 + 10 files changed, 624 insertions(+) create mode 100644 NETWORKING.md create mode 100644 examples/http-server-simple/main.orc create mode 100644 examples/http-server/src/main.orc create mode 100644 examples/tcp-client/src/main.orc create mode 100644 examples/tcp-echo-server/src/main.orc create mode 100644 orchid-std/src/std/socket/mod.rs create mode 100644 orchid-std/src/std/socket/socket_atom.rs create mode 100644 orchid-std/src/std/socket/socket_lib.rs diff --git a/NETWORKING.md b/NETWORKING.md new file mode 100644 index 0000000..8130183 --- /dev/null +++ b/NETWORKING.md @@ -0,0 +1,283 @@ +# Networking in Orchid + +This document explains how to use TCP networking in the Orchid programming language. + +## Prerequisites + +Build the project with the nightly Rust toolchain: + +```sh +rustup run nightly cargo build +``` + +## Socket Module Overview + +The socket module is available at `std::socket::tcp` and provides functions for TCP client/server communication. + +### Importing + +```orchid +import std::socket::tcp::(bind, accept, connect, read, write_all, close) +``` + +Or import all functions: + +```orchid +import std::socket::tcp +``` + +## API Reference + +### Server Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `bind` | `String -> TcpListener` | Create a TCP listener bound to an address | +| `accept` | `TcpListener -> TcpStream` | Accept an incoming connection | +| `listener_addr` | `TcpListener -> String` | Get the address the listener is bound to | + +### Client Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `connect` | `String -> TcpStream` | Connect to a TCP server | + +### Stream Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `read` | `TcpStream -> Int -> String` | Read up to N bytes as UTF-8 string | +| `read_bytes` | `TcpStream -> Int -> String` | Read up to N bytes as raw byte string | +| `read_exact` | `TcpStream -> Int -> String` | Read exactly N bytes | +| `write` | `TcpStream -> String -> Int` | Write data, returns bytes written | +| `write_all` | `TcpStream -> String -> Int` | Write all data | +| `flush` | `TcpStream -> Int` | Flush the stream buffer | +| `close` | `TcpStream -> Int` | Close the connection | +| `peer_addr` | `TcpStream -> String` | Get the remote peer's address | +| `local_addr` | `TcpStream -> String` | Get the local address | + +## Quick Start Examples + +### Testing Socket Bind + +```sh +rustup run nightly cargo orcx -- exec "std::socket::tcp::bind \"127.0.0.1:8080\"" +``` + +Output: +``` + +``` + +### Testing Connection (expect error if no server running) + +```sh +rustup run nightly cargo orcx -- exec "std::socket::tcp::connect \"127.0.0.1:8080\"" +``` + +Output (if no server): +``` +error: IO error: Connection refused (os error 111): @ +``` + +## Example Scripts + +### TCP Echo Server + +Location: `examples/tcp-echo-server/src/main.orc` + +```orchid +import std::socket::tcp::(bind, accept, read, write_all, close) + +const handle_client := \client. do cps { + cps data = read client 1024; + cps _ = write_all client data; + cps _ = close client; + cps pass 0; +} + +const accept_loop := \server. do cps { + cps client = accept server; + cps _ = handle_client client; + cps pass $ accept_loop server; +} + +const main := do cps { + cps server = bind "127.0.0.1:8080"; + cps pass $ accept_loop server; +} +``` + +Run the echo server: + +```sh +rustup run nightly cargo orcx -- exec --proj ./examples/tcp-echo-server "src::main::main" +``` + +### TCP Client + +Location: `examples/tcp-client/src/main.orc` + +```orchid +import std::socket::tcp::(connect, read, write_all, close, peer_addr) +import system::io::println + +const main := do cps { + cps conn = connect "127.0.0.1:8080"; + cps addr = peer_addr conn; + cps println $ "Connected to ${addr}"; + cps _ = write_all conn "Hello from Orchid!\n"; + cps response = read conn 1024; + cps println $ "Server response: ${response}"; + cps _ = close conn; + cps pass 0; +} +``` + +Run the client (requires a server running on port 8080): + +```sh +rustup run nightly cargo orcx -- exec --proj ./examples/tcp-client "src::main::main" +``` + +### HTTP Server + +Location: `examples/http-server/src/main.orc` + +```orchid +import std::socket::tcp::(bind, accept, read, write_all, close, peer_addr, listener_addr) +import system::io::println + +const http_response := "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 44\r\nConnection: close\r\n\r\n

Hello Orchid!

" + +const handle_client := \client. do cps { + cps addr = peer_addr client; + cps println $ "Client connected: ${addr}"; + cps request = read client 4096; + cps println $ "Received request from ${addr}"; + cps _ = write_all client http_response; + cps _ = close client; + cps println $ "Connection closed: ${addr}"; + cps pass 0; +} + +const accept_loop := \server. do cps { + cps client = accept server; + cps _ = handle_client client; + cps pass $ accept_loop server; +} + +const main := do cps { + cps server = bind "127.0.0.1:8080"; + cps addr = listener_addr server; + cps println $ "HTTP Server listening on ${addr}"; + cps println "Press Ctrl+C to stop"; + cps pass $ accept_loop server; +} +``` + +Run the HTTP server: + +```sh +rustup run nightly cargo orcx -- exec --proj ./examples/http-server "src::main::main" +``` + +Then open http://127.0.0.1:8080 in your browser or test with curl: + +```sh +curl http://127.0.0.1:8080 +``` + +## Understanding the CPS Syntax + +Orchid uses Continuation-Passing Style (CPS) for side effects. The `do cps { ... }` block is used for sequential operations: + +- `cps variable = expression;` - Bind the result of an expression to a variable +- `cps expression;` - Execute an expression for its side effects +- `cps pass value;` - Return a value from the block + +Example breakdown: + +```orchid +const main := do cps { + cps server = bind "127.0.0.1:8080"; -- Bind socket, store in 'server' + cps client = accept server; -- Accept connection, store in 'client' + cps data = read client 1024; -- Read data from client + cps _ = write_all client data; -- Write data back (ignore result) + cps _ = close client; -- Close connection + cps pass 0; -- Return 0 +} +``` + +## Error Handling + +Socket operations return errors when they fail. Errors are displayed with the IO error message: + +``` +error: IO error: Connection refused (os error 111): @ +``` + +Common errors: +- `Connection refused` - No server listening on the target address +- `Address already in use` - Port is already bound by another process +- `Connection reset by peer` - Remote end closed the connection unexpectedly + +## Building a Simple Protocol + +Here's an example of a simple request/response protocol: + +```orchid +import std::socket::tcp::(bind, accept, read, write_all, close) + +const handle_request := \request. + if request == "PING\n" then "PONG\n" + else if request == "TIME\n" then "2024-01-01 00:00:00\n" + else "UNKNOWN\n" + +const handle_client := \client. do cps { + cps request = read client 1024; + cps response = pass $ handle_request request; + cps _ = write_all client response; + cps _ = close client; + cps pass 0; +} + +const server_loop := \server. do cps { + cps client = accept server; + cps _ = handle_client client; + cps pass $ server_loop server; +} + +const main := do cps { + cps server = bind "127.0.0.1:9000"; + cps pass $ server_loop server; +} +``` + +## Limitations + +- The current implementation handles one client at a time (sequential accept loop) +- Sockets are non-serializable (cannot be persisted or transferred across boundaries) +- No UDP support (TCP only) +- No TLS/SSL support + +## Troubleshooting + +### Port Already in Use + +If you get "Address already in use", either: +1. Wait for the previous process to release the port +2. Use a different port +3. Kill the process using the port: `lsof -i :8080` then `kill ` + +### Connection Refused + +Ensure the server is running before starting the client. + +### Build Errors + +Make sure to use the nightly toolchain: + +```sh +rustup run nightly cargo build +``` diff --git a/examples/http-server-simple/main.orc b/examples/http-server-simple/main.orc new file mode 100644 index 0000000..9916237 --- /dev/null +++ b/examples/http-server-simple/main.orc @@ -0,0 +1,29 @@ +import std::socket::tcp::(bind, accept, read, write_all, close, peer_addr, listener_addr) +import system::io::println + +let http_response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 44\r\nConnection: close\r\n\r\n

Hello Orchid!

" + +let handle_client = \client. do cps { + cps addr = peer_addr client; + cps println $ "Client connected: ${addr}"; + cps request = read client 4096; + cps println $ "Received request from ${addr}"; + cps _ = write_all client http_response; + cps _ = close client; + cps println $ "Connection closed: ${addr}"; + cps pass 0; +} + +let accept_loop = \server. do cps { + cps client = accept server; + cps _ = handle_client client; + cps pass $ accept_loop server; +} + +let main = do cps { + cps server = bind "127.0.0.1:8080"; + cps addr = listener_addr server; + cps println $ "HTTP Server listening on ${addr}"; + cps println "Press Ctrl+C to stop"; + cps pass $ accept_loop server; +} diff --git a/examples/http-server/src/main.orc b/examples/http-server/src/main.orc new file mode 100644 index 0000000..084f411 --- /dev/null +++ b/examples/http-server/src/main.orc @@ -0,0 +1,31 @@ +import std::socket::tcp::(bind, accept, read, write_all, close, peer_addr, listener_addr) +import system::io::println + +cps println $ "Hello Orchid!"; + +const http_response := "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 44\r\nConnection: close\r\n\r\n

Hello Orchid!

" + +const handle_client := \client. do cps { + cps addr = peer_addr client; + cps println $ "Client connected: ${addr}"; + cps request = read client 4096; + cps println $ "Received request from ${addr}"; + cps _ = write_all client http_response; + cps _ = close client; + cps println $ "Connection closed: ${addr}"; + cps pass 0; +} + +const accept_loop := \server. do cps { + cps client = accept server; + cps _ = handle_client client; + cps pass $ accept_loop server; +} + +const main := do cps { + cps server = bind "127.0.0.1:8080"; + cps addr = listener_addr server; + cps println $ "HTTP Server listening on ${addr}"; + cps println "Press Ctrl+C to stop"; + cps pass $ accept_loop server; +} diff --git a/examples/tcp-client/src/main.orc b/examples/tcp-client/src/main.orc new file mode 100644 index 0000000..d0cb998 --- /dev/null +++ b/examples/tcp-client/src/main.orc @@ -0,0 +1,13 @@ +import std::socket::tcp::(connect, read, write_all, close, peer_addr) +import system::io::println + +const main := do cps { + cps conn = connect "127.0.0.1:8080"; + cps addr = peer_addr conn; + cps println $ "Connected to ${addr}"; + cps _ = write_all conn "Hello from Orchid!\n"; + cps response = read conn 1024; + cps println $ "Server response: ${response}"; + cps _ = close conn; + cps pass 0; +} diff --git a/examples/tcp-echo-server/src/main.orc b/examples/tcp-echo-server/src/main.orc new file mode 100644 index 0000000..544de10 --- /dev/null +++ b/examples/tcp-echo-server/src/main.orc @@ -0,0 +1,19 @@ +import std::socket::tcp::(bind, accept, read, write_all, close) + +const handle_client := \client. do cps { + cps data = read client 1024; + cps _ = write_all client data; + cps _ = close client; + cps pass 0; +} + +const accept_loop := \server. do cps { + cps client = accept server; + cps _ = handle_client client; + cps pass $ accept_loop server; +} + +const main := do cps { + cps server = bind "127.0.0.1:8080"; + cps pass $ accept_loop server; +} diff --git a/orchid-std/src/std/mod.rs b/orchid-std/src/std/mod.rs index 7087cc2..25e3040 100644 --- a/orchid-std/src/std/mod.rs +++ b/orchid-std/src/std/mod.rs @@ -3,6 +3,7 @@ pub mod option; pub mod protocol; pub mod record; pub mod reflection; +pub mod socket; pub mod std_system; pub mod string; pub mod tuple; diff --git a/orchid-std/src/std/socket/mod.rs b/orchid-std/src/std/socket/mod.rs new file mode 100644 index 0000000..5c8728c --- /dev/null +++ b/orchid-std/src/std/socket/mod.rs @@ -0,0 +1,2 @@ +pub mod socket_atom; +pub mod socket_lib; diff --git a/orchid-std/src/std/socket/socket_atom.rs b/orchid-std/src/std/socket/socket_atom.rs new file mode 100644 index 0000000..cea829e --- /dev/null +++ b/orchid-std/src/std/socket/socket_atom.rs @@ -0,0 +1,110 @@ +use std::borrow::Cow; +use std::rc::Rc; +use std::cell::RefCell; + +use futures::lock::Mutex; +use never::Never; +use orchid_base::format::{FmtCtx, FmtUnit}; +use orchid_extension::atom::{Atomic, MethodSetBuilder}; +use orchid_extension::atom_owned::{OwnedAtom, OwnedVariant}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; + +pub struct TcpListenerAtom { + pub listener: Rc>, + pub addr: String, +} + +impl Clone for TcpListenerAtom { + fn clone(&self) -> Self { + Self { + listener: self.listener.clone(), + addr: self.addr.clone(), + } + } +} + +impl Atomic for TcpListenerAtom { + type Variant = OwnedVariant; + type Data = (); + fn reg_reqs() -> MethodSetBuilder { MethodSetBuilder::new() } +} + +impl OwnedAtom for TcpListenerAtom { + type Refs = Never; + async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) } + async fn print_atom<'a>(&'a self, _: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit { + format!("", self.addr).into() + } +} + +pub struct TcpStreamAtom { + pub stream: Rc>>, + pub peer_addr: String, + pub local_addr: String, +} + +impl Clone for TcpStreamAtom { + fn clone(&self) -> Self { + Self { + stream: self.stream.clone(), + peer_addr: self.peer_addr.clone(), + local_addr: self.local_addr.clone(), + } + } +} + +impl Atomic for TcpStreamAtom { + type Variant = OwnedVariant; + type Data = (); + fn reg_reqs() -> MethodSetBuilder { MethodSetBuilder::new() } +} + +impl OwnedAtom for TcpStreamAtom { + type Refs = Never; + async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) } + async fn print_atom<'a>(&'a self, _: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit { + format!(" {}>", self.local_addr, self.peer_addr).into() + } +} + +impl TcpStreamAtom { + pub async fn read(&self, max_bytes: usize) -> std::io::Result> { + let mut stream_opt = self.stream.borrow_mut(); + let stream = stream_opt.as_mut().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotConnected, "Stream closed") + })?; + let mut buf = vec![0u8; max_bytes]; + let n = stream.read(&mut buf).await?; + buf.truncate(n); + Ok(buf) + } + + pub async fn write(&self, data: &[u8]) -> std::io::Result { + let mut stream_opt = self.stream.borrow_mut(); + let stream = stream_opt.as_mut().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotConnected, "Stream closed") + })?; + stream.write(data).await + } + + pub async fn write_all(&self, data: &[u8]) -> std::io::Result<()> { + let mut stream_opt = self.stream.borrow_mut(); + let stream = stream_opt.as_mut().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotConnected, "Stream closed") + })?; + stream.write_all(data).await + } + + pub async fn flush(&self) -> std::io::Result<()> { + let mut stream_opt = self.stream.borrow_mut(); + let stream = stream_opt.as_mut().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotConnected, "Stream closed") + })?; + stream.flush().await + } + + pub fn close(&self) { + let _ = self.stream.borrow_mut().take(); + } +} diff --git a/orchid-std/src/std/socket/socket_lib.rs b/orchid-std/src/std/socket/socket_lib.rs new file mode 100644 index 0000000..5b3fb89 --- /dev/null +++ b/orchid-std/src/std/socket/socket_lib.rs @@ -0,0 +1,131 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use futures::lock::Mutex; +use orchid_base::error::{OrcRes, OrcErrv, mk_errv_floating}; +use orchid_extension::atom::TAtom; +use orchid_extension::atom_owned::own; +use orchid_extension::context::i; +use orchid_extension::conv::TryFromExpr; +use orchid_extension::expr::Expr; +use orchid_extension::tree::{GenMember, fun, prefix}; +use tokio::net::{TcpListener, TcpStream}; + +use super::socket_atom::{TcpListenerAtom, TcpStreamAtom}; +use crate::std::string::str_atom::StrAtom; +use crate::{Int, OrcString}; + +async fn io_err(e: std::io::Error) -> OrcErrv { + mk_errv_floating(i().i(&format!("IO error: {}", e)).await, "") +} + +macro_rules! try_io { + ($e:expr) => { + match $e { + Ok(v) => v, + Err(e) => return Err(io_err(e).await), + } + }; +} + +impl TryFromExpr for TcpListenerAtom { + async fn try_from_expr(expr: Expr) -> OrcRes { + let tatom = TAtom::::try_from_expr(expr).await?; + Ok(own(&tatom).await) + } +} + +impl TryFromExpr for TcpStreamAtom { + async fn try_from_expr(expr: Expr) -> OrcRes { + let tatom = TAtom::::try_from_expr(expr).await?; + Ok(own(&tatom).await) + } +} + +pub fn gen_socket_lib() -> Vec { + prefix("std::socket", [ + prefix("tcp", [ + fun(true, "bind", async |addr: OrcString| -> OrcRes { + let addr_str = addr.get_string().await; + let listener = try_io!(TcpListener::bind(addr_str.as_str()).await); + let local_addr = listener.local_addr().map(|a| a.to_string()).unwrap_or_default(); + Ok(TcpListenerAtom { + listener: Rc::new(Mutex::new(listener)), + addr: local_addr, + }) + }), + fun(true, "accept", async |listener: TcpListenerAtom| -> OrcRes { + let guard = listener.listener.lock().await; + let (stream, peer_addr) = try_io!(guard.accept().await); + let local_addr = stream.local_addr().map(|a| a.to_string()).unwrap_or_default(); + Ok(TcpStreamAtom { + stream: Rc::new(RefCell::new(Some(stream))), + peer_addr: peer_addr.to_string(), + local_addr, + }) + }), + fun(true, "connect", async |addr: OrcString| -> OrcRes { + let addr_str = addr.get_string().await; + let stream = try_io!(TcpStream::connect(addr_str.as_str()).await); + let peer_addr = stream.peer_addr().map(|a| a.to_string()).unwrap_or_default(); + let local_addr = stream.local_addr().map(|a| a.to_string()).unwrap_or_default(); + Ok(TcpStreamAtom { + stream: Rc::new(RefCell::new(Some(stream))), + peer_addr, + local_addr, + }) + }), + fun(true, "read", async |stream: TcpStreamAtom, max_bytes: Int| -> OrcRes { + let data = try_io!(stream.read(max_bytes.0 as usize).await); + let s = String::from_utf8_lossy(&data).to_string(); + Ok(StrAtom::new(Rc::new(s))) + }), + fun(true, "read_bytes", async |stream: TcpStreamAtom, max_bytes: Int| -> OrcRes { + let data = try_io!(stream.read(max_bytes.0 as usize).await); + let s = data.iter().map(|b| *b as char).collect::(); + Ok(StrAtom::new(Rc::new(s))) + }), + fun(true, "read_exact", async |stream: TcpStreamAtom, num_bytes: Int| -> OrcRes { + let mut result = Vec::new(); + let target = num_bytes.0 as usize; + while result.len() < target { + let remaining = target - result.len(); + let chunk = try_io!(stream.read(remaining).await); + if chunk.is_empty() { + return Err(mk_errv_floating(i().i("Connection closed before receiving all bytes").await, "")); + } + result.extend(chunk); + } + let s = String::from_utf8_lossy(&result).to_string(); + Ok(StrAtom::new(Rc::new(s))) + }), + fun(true, "write", async |stream: TcpStreamAtom, data: OrcString| -> OrcRes { + let data_str = data.get_string().await; + let n = try_io!(stream.write(data_str.as_bytes()).await); + Ok(Int(n as i64)) + }), + fun(true, "write_all", async |stream: TcpStreamAtom, data: OrcString| -> OrcRes { + let data_str = data.get_string().await; + try_io!(stream.write_all(data_str.as_bytes()).await); + Ok(Int(0)) + }), + fun(true, "flush", async |stream: TcpStreamAtom| -> OrcRes { + try_io!(stream.flush().await); + Ok(Int(0)) + }), + fun(true, "close", async |stream: TcpStreamAtom| -> Int { + stream.close(); + Int(0) + }), + fun(true, "peer_addr", async |stream: TcpStreamAtom| { + StrAtom::new(Rc::new(stream.peer_addr.clone())) + }), + fun(true, "local_addr", async |stream: TcpStreamAtom| { + StrAtom::new(Rc::new(stream.local_addr.clone())) + }), + fun(true, "listener_addr", async |listener: TcpListenerAtom| { + StrAtom::new(Rc::new(listener.addr.clone())) + }), + ]), + ]) +} diff --git a/orchid-std/src/std/std_system.rs b/orchid-std/src/std/std_system.rs index 6d692cf..12f9cd8 100644 --- a/orchid-std/src/std/std_system.rs +++ b/orchid-std/src/std/std_system.rs @@ -27,6 +27,8 @@ use crate::std::protocol::types::{Tag, Tagged, gen_protocol_lib}; use crate::std::record::record_atom::Record; use crate::std::record::record_lib::gen_record_lib; use crate::std::reflection::sym_atom::{CreateSymAtom, SymAtom, gen_sym_lib}; +use crate::std::socket::socket_atom::{TcpListenerAtom, TcpStreamAtom}; +use crate::std::socket::socket_lib::gen_socket_lib; use crate::std::string::str_lexer::StringLexer; use crate::std::string::to_string::AsStrTag; use crate::std::tuple::{CreateTuple, Tuple, TupleBuilder, gen_tuple_lib}; @@ -64,6 +66,8 @@ impl SystemCard for StdSystem { Some(Tag::dynfo()), Some(Tagged::dynfo()), Some(AsStrTag::dynfo()), + Some(TcpListenerAtom::dynfo()), + Some(TcpStreamAtom::dynfo()), ] } } @@ -92,6 +96,7 @@ impl System for StdSystem { gen_tuple_lib(), gen_protocol_lib(), gen_sym_lib().await, + gen_socket_lib(), ]) } async fn prelude() -> Vec {