Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

libc Shims

libc structs hold raw pointers, which are incompatible with the refcounted pointers the refcount model uses, so a libc struct cannot be used directly. The shim modules therefore define Rust counterparts for the libc types translated programs use. A shim struct mirrors its C struct member by member, like a translated struct: fields are stored inline, except for arrays, which are Value<Box<[T]>>s of their own (see Boxing). A shim converts to or from the underlying libc or nix type at the call boundary.

Stat is a typical shim:

#![allow(unused)]
fn main() {
#[derive(Clone, Default, Record)]
pub struct Stat {
    #[offset(offset_of!(::libc::stat, st_dev))]
    pub st_dev: u64,
    #[offset(offset_of!(::libc::stat, st_ino))]
    pub st_ino: u64,
    // ...
    #[offset(offset_of!(::libc::stat, st_size))]
    pub st_size: i64,
}

impl Stat {
    pub fn from_libc(s: &::libc::stat) -> Self { /* ... */ }
}
}

A stat call in the source program becomes a nix::sys::stat::stat call. On success nix returns a raw libc::stat, so the result goes through Stat::from_libc before it is written into the translated struct.

Like translated structs, shims derive Record, so that pointers to their fields can be taken (see Pointers to fields). The offsets of the fields are those of the libc struct, given by offset_of!, and their ByteRepr gives the size of the libc struct, which locates the fields of the elements of an array of shims, like an array of pollfd. The sockaddr family, whose byte representation has a layout of its own, uses the offsets of that layout.

The modules

Each shim lives next to the rules that use it, as rules/<dir>/shim.rs. The libcc2rs build script finds every such file and includes it as a module of the crate, re-exported at the crate root, so a shim refers to other runtime items through crate:: and translated code reaches it as libcc2rs::Stat.

Rule dirC types
stdioFILE (CFile)
direntstruct dirent, DIR (Dirent, CDir)
selectfd_set (CFdSet)
ifaddrsstruct ifaddrs (Ifaddrs)
ipstruct in_addr, struct in6_addr (InAddr, In6Addr)
netdbstruct addrinfo (Addrinfo)
pollstruct pollfd (Pollfd)
pwdstruct passwd (Passwd)
socketthe sockaddr family (Sockaddr, SockaddrIn, SockaddrIn6, SockaddrUn, SockaddrStorage)
statstruct stat (Stat)
termiosstruct termios, struct winsize (Termios, Winsize)
timestruct tm, struct timeval, struct timespec (Tm, Timeval, Timespec)

Most shims are plain data plus conversions like Stat. CFile carries the stdio stream logic (see I/O and Formatting), and the time shims convert through the jiff crate. CFdSet and the sockaddr family need more than a field-by-field mirror and are described in their own sections below.

Each shim file also gives the raw libc struct it mirrors an empty ByteRepr impl (impl ByteRepr for ::libc::stat {}), whose methods panic. These exist so that the generated ByteRepr implementation of a translated struct with a libc struct member still compiles; reinterpreting such a struct is not supported at present.

CFdSet

nix has its own FdSet, but it is stricter than the C one: it ties the set to the lifetimes of the descriptors it holds. A C fd_set is just a set of integers that accepts anything; whether the descriptors are valid is only checked by the select call that eventually receives the set. CFdSet keeps the C behavior by storing plain integers, and the select rule builds the nix FdSet from it at call time.

The sockaddr family

C socket code reinterprets one address struct as another: the program fills in a struct sockaddr_in, passes it to bind as a struct sockaddr *, and casts back to the concrete type on the way out of accept. The address shims keep this pattern working by implementing ByteRepr with the exact byte layout of their C structs: the family in the first two bytes, the remaining members at their C offsets. A cast in the source program becomes a reinterpret_cast on the refcounted pointer, which reads the struct through that byte layout as the target type, so any member of the family can be viewed as any other, exactly as in C.

The call boundary works the same way. Sockaddr::decode reads the family from the first two bytes and reinterprets the pointer as the concrete type before handing nix a typed address:

#![allow(unused)]
fn main() {
pub fn decode(
    addr: &Ptr<Sockaddr>,
    _len: u32,
) -> Option<Box<dyn SockaddrLike>> {
    let family = addr.reinterpret_cast::<u16>().read();
    if family == libc::AF_INET as u16 {
        let m = addr.reinterpret_cast::<SockaddrIn>().read();
        Some(Box::new(nix::sys::socket::SockaddrIn::from(m.to_libc())))
    }
    // ... AF_INET6 and AF_UNIX in the same way ...
}
}

Sockaddr::encode goes the other way, writing an address returned by nix into the caller’s buffer through the concrete shim. Ifaddrs hands out its addresses as Ptr<Sockaddr> values ready to be reinterpreted.

Non-uniform fields

Some struct fields are not spelled the same on every platform. struct stat keeps the modification time in a nested struct timespec, named st_mtim on Linux and st_mtimespec on macOS, while the shim exposes a single st_mtime field. struct in6_addr hides its bytes behind the internal __in6_u union on Linux, while the shim exposes s6_addr. The shims pick one uniform field, and the code generator meets them halfway: replaceNonUniformLibcField in the converter rewrites the platform-specific member chain in the source, so st.st_mtim.tv_sec becomes st.st_mtime in the translated code.