Initial networking module. #1

Open
retoor wants to merge 1 commits from networking into main
10 changed files with 624 additions and 0 deletions

283
NETWORKING.md Normal file
View File

@@ -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:
```
<TcpListener 127.0.0.1:8080>
```
### 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<html><body><h1>Hello Orchid!</h1></body></html>"
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 <pid>`
### 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
```

View File

@@ -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<html><body><h1>Hello Orchid!</h1></body></html>"
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;
}

View File

@@ -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<html><body><h1>Hello Orchid!</h1></body></html>"
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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -3,6 +3,7 @@ pub mod option;
pub mod protocol; pub mod protocol;
pub mod record; pub mod record;
pub mod reflection; pub mod reflection;
pub mod socket;
pub mod std_system; pub mod std_system;
pub mod string; pub mod string;
pub mod tuple; pub mod tuple;

View File

@@ -0,0 +1,2 @@
pub mod socket_atom;
pub mod socket_lib;

View File

@@ -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<Mutex<TcpListener>>,
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<Self> { 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!("<TcpListener {}>", self.addr).into()
}
}
pub struct TcpStreamAtom {
pub stream: Rc<RefCell<Option<TcpStream>>>,
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<Self> { 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!("<TcpStream {} -> {}>", self.local_addr, self.peer_addr).into()
}
}
impl TcpStreamAtom {
pub async fn read(&self, max_bytes: usize) -> std::io::Result<Vec<u8>> {
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<usize> {
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();
}
}

View File

@@ -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<Self> {
let tatom = TAtom::<TcpListenerAtom>::try_from_expr(expr).await?;
Ok(own(&tatom).await)
}
}
impl TryFromExpr for TcpStreamAtom {
async fn try_from_expr(expr: Expr) -> OrcRes<Self> {
let tatom = TAtom::<TcpStreamAtom>::try_from_expr(expr).await?;
Ok(own(&tatom).await)
}
}
pub fn gen_socket_lib() -> Vec<GenMember> {
prefix("std::socket", [
prefix("tcp", [
fun(true, "bind", async |addr: OrcString| -> OrcRes<TcpListenerAtom> {
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<TcpStreamAtom> {
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<TcpStreamAtom> {
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<StrAtom> {
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<StrAtom> {
let data = try_io!(stream.read(max_bytes.0 as usize).await);
let s = data.iter().map(|b| *b as char).collect::<String>();
Ok(StrAtom::new(Rc::new(s)))
}),
fun(true, "read_exact", async |stream: TcpStreamAtom, num_bytes: Int| -> OrcRes<StrAtom> {
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<Int> {
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<Int> {
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<Int> {
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()))
}),
]),
])
}

View File

@@ -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_atom::Record;
use crate::std::record::record_lib::gen_record_lib; use crate::std::record::record_lib::gen_record_lib;
use crate::std::reflection::sym_atom::{CreateSymAtom, SymAtom, gen_sym_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::str_lexer::StringLexer;
use crate::std::string::to_string::AsStrTag; use crate::std::string::to_string::AsStrTag;
use crate::std::tuple::{CreateTuple, Tuple, TupleBuilder, gen_tuple_lib}; use crate::std::tuple::{CreateTuple, Tuple, TupleBuilder, gen_tuple_lib};
@@ -64,6 +66,8 @@ impl SystemCard for StdSystem {
Some(Tag::dynfo()), Some(Tag::dynfo()),
Some(Tagged::dynfo()), Some(Tagged::dynfo()),
Some(AsStrTag::dynfo()), Some(AsStrTag::dynfo()),
Some(TcpListenerAtom::dynfo()),
Some(TcpStreamAtom::dynfo()),
] ]
} }
} }
@@ -92,6 +96,7 @@ impl System for StdSystem {
gen_tuple_lib(), gen_tuple_lib(),
gen_protocol_lib(), gen_protocol_lib(),
gen_sym_lib().await, gen_sym_lib().await,
gen_socket_lib(),
]) ])
} }
async fn prelude() -> Vec<Sym> { async fn prelude() -> Vec<Sym> {