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.