Rust doesn’t live in a vacuum and your libraries may need to interact with the rest of the world. Behind sophisticated types and abstractions there is always raw communication and data exchange. What if you are the one to wrap the low-level calls into pretty abstractions?

Talking to the C runtime

Whenever you need to talk to the C library you need to call the native functions from Rust.

extern "C" {
    fn malloc(size: usize) -> *mut libc::c_void;
}

fn main() {
    let x = unsafe { malloc(64) };
    println!("{:?}", x);
}

This function just calls a standard function to allocate 64 bytes of memory, and prints the result. You see that calling external functions is unsafe because you have no safety information.

Many of the standard functions are available via the libc Rust library so that you don’t need to declare all the external functions.

fn main() {
    let x = unsafe { libc::malloc(64) };
    println!("{:?}", x);
}

Talking to the operating system

Some of the library functions are just wrappers over system calls to communicate with the kernel. An interesting example is sendmsg() and recvmsg() that provide an extended API to send data over the network.

You need to use a socket in order to experiment with these APIs. There is a simple call that creates two connected sockets in Linux. Similar tools are available in other systems.

fn main() {
    let mut socks = [0; 2];
    let ret = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_SEQPACKET, 0, socks.as_mut_ptr()) };
    println!("{:?}", ret);
}

This is not how you would expect to create a pair of sockets in Rust, so let’s wrap it properly.

enum Family {
    Unix,
}

enum SocketType {
    SeqPacket,
}

enum Protocol {
    None,
}


fn main() {
    let (left, right) = socketpair(Family::Unix, SocketType::SeqPacket, Protocol::None).unwrap();
}

Now the interface is not that ugly. It could still be improved but let us be modest.

Forking processes

You could skip this if you were happy with threads or Tokio tasks. But what if you need to start new clones of the current process? Most operating systems support a system call like fork or clone that is called from one process but (after cloning the process) returns to the two separate copies of the original process.

struct ForkedProcess(i32);

fn spawn(child: impl FnOnce() -> ()) -> Result<ForkedProcess, io::Error> {
    let ret = unsafe { libc::fork() };
    match ret {
        -1 => Err(io::Error::last_os_error()),
        0 => {
            child();
            unsafe { libc::exit(0) };
        }
        pid => Ok(ForkedProcess(pid)),
    }
}

fn main() {
    let (left, right) = socketpair(Family::Unix, SocketType::SeqPacket, Protocol::None).unwrap();
    let child = spawn(|| {
        println!("child!");
    }).unwrap();
}

This is how a fork or clone system call could be wrapped for Rust users.

Sending messages

For sending messages over sockets you could use anything ranging from the standard library through Tokio to the standard send, recv system calls. Let us move beyond that and also skip sendto and recvfrom and jump right to the most advanced interface.

When you use sendmsg and recvmsg, you need to supply a sophisticated data structure that supports not just sending a buffer but sending a list of buffers and asking the kernel for additional processing. You can see that the code is rather complex. Explanation follows.

use std::ptr::null_mut;

#[derive(Copy, Clone, Debug, PartialEq)]
enum Message {
    None,
    Request,
}

impl Socket {
    fn close(self) {
        let ret = unsafe { libc::close(self.fd) };
        match ret {
            0 => (),
            _ => panic!(),
        }
    }

    fn send(&mut self, mut message: Message) -> Result<(), io::Error> {
        let vecs = [libc::iovec {
            iov_base: (&mut message) as *mut Message as *mut libc::c_void,
            iov_len: std::mem::size_of_val(&message),
        }; 1];
        let msg = libc::msghdr {
            msg_name: null_mut(),
            msg_namelen: 0,
            msg_iov: vecs.as_ptr() as *mut libc::iovec,
            msg_iovlen: vecs.len(),
            msg_control: null_mut(),
            msg_controllen: 0,
            msg_flags: 0,
        };
        let flags = 0;
        let ret = unsafe { libc::sendmsg(self.fd, &msg, flags) };
        match ret {
            -1 => Err(io::Error::last_os_error()),
            _bytes => Ok(()),
        }
    }

    fn receive(&mut self) -> Result<Message, io::Error> {
        let mut message = Message::None;
        let vecs = [libc::iovec {
            iov_base: (&mut message) as *mut Message as *mut libc::c_void,
            iov_len: std::mem::size_of_val(&message),
        }; 1];
        let mut msg = libc::msghdr {
            msg_name: null_mut(),
            msg_namelen: 0,
            msg_iov: vecs.as_ptr() as *mut libc::iovec,
            msg_iovlen: vecs.len(),
            msg_control: null_mut(),
            msg_controllen: 0,
            msg_flags: 0,
        };
        let flags = 0;
        let ret = unsafe { libc::recvmsg(self.fd, &mut msg, flags) };
        match ret {
            -1 => Err(io::Error::last_os_error()),
            _ => Ok(message),
        }
    }
}

struct ForkedProcess {
    pid: i32,
    socket: Socket,
}

impl ForkedProcess {
    fn spawn(child: impl FnOnce(Socket) -> (), sockets: (Socket, Socket)) -> Result<ForkedProcess, io::Error> {
        let (left, right) = sockets;
        let ret = unsafe { libc::fork() };
        match ret {
            -1 => Err(io::Error::last_os_error()),
            0 => {
                right.close();
                child(left);
                unsafe { libc::exit(0) };
            }
            pid => {
                left.close();
                Ok(ForkedProcess{ pid, socket: right })
            }
        }
    }

    fn join(self) {
        self.socket.close();
        let ret = unsafe { libc::waitpid(self.pid, null_mut(), 0) };
        match ret {
            -1 => panic!(),
            _pid => (),
        }
    }
}

fn main() {
    let sockets = socketpair(Family::Unix, SocketType::SeqPacket, Protocol::None).unwrap();

    let mut child = ForkedProcess::spawn(|mut sock| {
        sock.send(Message::Request).unwrap();
    }, sockets).unwrap();

    let message = child.socket.receive().unwrap();
    println!("{:?}", message);

    child.join();
}

Both sendmsg and recvmsg are almost identical in the way they are called. You can examine the code and see what needs to be done to pass data between Rust and the kernel or the C libraries.