Created
September 8, 2026 11:28
-
-
Save eddiebergman/adb68c85e05ae34dd70a9cd3323e9ec7 to your computer and use it in GitHub Desktop.
Brief learning example of how to use macos libc calls to handle a network socket
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ---cargo | |
| [dependencies] | |
| libc = "0.2.*" | |
| --- | |
| // Minimal but functional example of socket read/write loop on macos for ipv4, ~100 LoC | |
| // Run standalone with: cargo +nightly -Zscript socket-listen.rs, delete frontmatter otherwise | |
| // Can test with `nc 127.0.0.1 9999` for interactive, `printf "hello\n" | nc ...` one shot | |
| // Hand-written, references from rust-lang/socket2, Gemini and various linux man pages | |
| // Notably not handled, async, threading, any error recovery | |
| use std::cmp::Ordering; | |
| use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; | |
| macro_rules! syscall { // rust-lang/socket2 | |
| ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{ | |
| #[allow(unused_unsafe)] | |
| let res = unsafe { libc::$fn($($arg, )*) }; | |
| if res == -1 { Err(std::io::Error::last_os_error()) } else { Ok(res) } | |
| }}; | |
| } | |
| fn main() -> Result<(), std::io::Error> { | |
| // https://stackoverflow.com/a/60425748 | |
| let socket = syscall!(socket(libc::AF_INET, libc::SOCK_STREAM, 0))?; | |
| let fd = unsafe { OwnedFd::from_raw_fd(socket) }; // Will auto-close | |
| let raw_fd = fd.as_raw_fd(); | |
| // macos: set CLOEXEC flag on the file descriptor (on linux, can set on creation) | |
| let prev = syscall!(fcntl(raw_fd, libc::F_GETFD))?; | |
| syscall!(fcntl(raw_fd, libc::F_SETFD, prev | libc::FD_CLOEXEC))?; | |
| // https://man7.org/linux/man-pages/man7/socket.7.html | |
| let set_opt = |opt: libc::c_int| -> std::io::Result<libc::c_int> { | |
| let enable: libc::c_int = 1; | |
| syscall!(setsockopt( | |
| raw_fd, | |
| libc::SOL_SOCKET, | |
| opt, | |
| (&raw const enable).cast(), // ptr to c_int as c_void | |
| std::mem::size_of::<libc::c_int>() as _, | |
| )) | |
| }; | |
| set_opt(libc::SO_NOSIGPIPE)?; // Prevent dead pipe causing SIGPIPE process interupt | |
| set_opt(libc::SO_REUSEADDR)?; // Allow immediate re-bind on the port on server restart | |
| // Very explicitly not making flexible, only ipv4 | |
| // Special note on ensuring big-endian (network-endian) with `to_be()` | |
| let address: std::net::SocketAddrV4 = "0.0.0.0:9999".parse().expect("parsable address"); | |
| let sin = libc::sockaddr_in { | |
| sin_len: std::mem::size_of::<libc::sockaddr_in>() as u8, | |
| sin_family: libc::AF_INET as _, | |
| sin_port: address.port().to_be() as _, | |
| sin_addr: libc::in_addr { | |
| s_addr: address.ip().to_bits().to_be(), | |
| }, | |
| sin_zero: [0; 8], | |
| }; | |
| // Bind and listen so it's ready to accept incoming connections | |
| syscall!(bind( | |
| raw_fd, | |
| (&raw const sin).cast(), | |
| std::mem::size_of::<libc::sockaddr_in>() as _, | |
| ))?; | |
| syscall!(listen(raw_fd, 128))?; | |
| // Loop accepting from our listening socket | |
| 'accept: loop { | |
| let mut client_addr: libc::sockaddr_storage = unsafe { std::mem::zeroed() }; | |
| let mut client_addr_len = std::mem::size_of::<libc::sockaddr_storage>(); | |
| let client_raw = syscall!(accept( | |
| fd.as_raw_fd(), | |
| (&raw mut client_addr).cast(), | |
| (&raw mut client_addr_len).cast(), | |
| ))?; | |
| let _client_fd = unsafe { OwnedFd::from_raw_fd(client_raw) }; // auto-close on Drop | |
| 'session: loop { | |
| let mut req: Vec<u8> = Vec::new(); | |
| let mut buffer = [0u8; 1024]; | |
| 'read: loop { | |
| let bytes_read = | |
| unsafe { libc::read(client_raw, buffer.as_mut_ptr().cast(), buffer.len()) }; | |
| match bytes_read.cmp(&0) { | |
| Ordering::Less => { | |
| let err = std::io::Error::last_os_error(); | |
| eprintln!("Error reading: {err:?}"); | |
| break 'session; | |
| } | |
| Ordering::Equal => break 'session, // Disconnect | |
| Ordering::Greater => match buffer.iter().position(|&b| b == b'\n') { | |
| Some(i) => { | |
| req.extend_from_slice(&buffer[..i]); | |
| break 'read; | |
| } | |
| None => req.extend_from_slice(&buffer[..bytes_read as usize]), | |
| }, | |
| } | |
| } | |
| let s = std::str::from_utf8(req.as_slice()).expect("valid utf8"); | |
| println!("Got {:?}", s); | |
| match s { | |
| "done" => break 'session, | |
| "shutdown" => break 'accept, | |
| _ => { | |
| let mut buf = (if s == "hi" { "hello\n" } else { "type hi\n" }).as_bytes(); | |
| while !buf.is_empty() { | |
| let bytes_written = | |
| unsafe { libc::write(client_raw, buf.as_ptr().cast(), buf.len()) }; | |
| if bytes_written > 0 { | |
| buf = &buf[bytes_written as usize..]; | |
| } else { | |
| let err = std::io::Error::last_os_error(); | |
| eprintln!("Error writing: {err:?}"); | |
| break 'session; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment