In order to achieve our goal of wrapping libc code in idiomatic rust constructs with minimal performance overhead, we follow the following conventions.
Note that, thus far, not all the code follows these conventions and not all conventions we try to follow have been documented here. If you find an instance of either, feel free to remedy the flaw by opening a pull request with appropriate changes or additions.
We follow the conventions laid out in Keep A CHANGELOG.
We do not define integer constants ourselves, but use or reexport them from the libc crate.
We use the functions exported from libc instead of writing our own
extern
declarations.
We use the struct
definitions from libc internally instead of writing
our own. If we want to add methods to a libc type, we use the newtype pattern.
For example,
pub struct SigSet(libc::sigset_t);
impl SigSet {
...
}
When creating newtypes, we use Rust's CamelCase
type naming convention.
When creating operating-system-specific functionality, we gate it by
#[cfg(target_os = ...)]
. If more than one operating system is affected, we
prefer to use the cfg aliases defined in build.rs, like #[cfg(bsd)]
.
Many C functions have flags parameters that are combined from constants using
bitwise operations. We represent the types of these parameters by types defined
using our libc_bitflags!
macro, which is a convenience wrapper around the
bitflags!
macro from the bitflags crate that brings in the
constant value from libc
.
We name the type for a set of constants whose element's names start with FOO_
FooFlags
.
For example,
libc_bitflags!{
pub struct ProtFlags: libc::c_int {
PROT_NONE;
PROT_READ;
PROT_WRITE;
PROT_EXEC;
#[cfg(any(target_os = "linux", target_os = "android"))]
PROT_GROWSDOWN;
#[cfg(any(target_os = "linux", target_os = "android"))]
PROT_GROWSUP;
}
}
We represent sets of constants that are intended as mutually exclusive arguments to parameters of functions by enumerations.
Whenever we need to use a libc function to properly initialize a
variable and said function allows us to use uninitialized memory, we use
std::mem::MaybeUninit
when defining the variable. This
allows us to avoid the overhead incurred by zeroing or otherwise initializing
the variable.
We prefer cast()
, cast_mut()
and cast_const()
to cast pointer types
over the as
keyword because it is much more difficult to accidentally change
type or mutability that way.
In Nix, if we want to remove something, we don't do it immediately, instead, we deprecate it for at least one release before removing it.
To deprecate an interface, put the following attribute on the top of it:
#[deprecated(since = "<Version>", note = "<Note to our user>")]
<Version>
is the version where this interface will be deprecated, in most
cases, it will be the version of the next release. And a user-friendly note
should be added. Normally, there should be a new interface that will replace
the old one, so a note should be something like: " should be
used instead".