lrs is a highly experimental, linux-only standard library for the rustc compiler. It does not use any parts of the standard library that is part of the rust distribution.
Since lrs is based on the rust compiler, it shares many features with rust (e.g., lifetimes, borrow checking, integer overflow checking, etc.) But rustc allows us to make significant changes to the language as long as we don’t use Mozilla’s standard library. This section lists some of the differences between rust and lrs and other features of lrs.
Note
|
In this section we’ll compare programs compiled against lrs and programs compiled against the "standard" standard library that comes with the rust distribution. To make things simpler, we will call programs that use lrs "lrs programs" and programs that use Mozilla’s standard library "rust programs". It should be clear from the context what is meant. |
Unwinding and the panic
macro have been removed from lrs. This means that
error handling works via return values or—in the case of unrecoverable
errors—process termination. This has the following advantages:
- Potentially better performance
-
Consider the following function:
fn f(a: &mut u8, b: &mut u8, g: fn()) { mem::swap(a, b); g(); mem::swap(a, b); }
If
g
cannot unwind, then this function can be optimized by removing bothswap
calls. But ifg
can unwind, then theswap
calls must stay in place since destructors called during unwinding might accessa
andb
. - No exception unsafety
-
Consider the following (incorrect) rust code:
fn push(a: &mut Vec<T>, g: fn() -> T) { unsafe { assert!(a.capacity() > a.len()); let len = a.len(); a.set_len(len + 1); // <-- BUG ptr::write(a.as_mut_ptr().offset(len as isize), g()); } }
This is a naive implementation of a non-allocating
push
method onVec<T>
. The code is incorrect because the length of the vector is increased before the return value ofg
has been written to it. Ifg
unwinds, the destructor ofVec<T>
will access the invalid value ata[len]
, which is likely undefined behavior. This problem does not exist in lrs. See this (long) thread for a discussion of exception safety in rust.
lrs programs usually compile down to executable with a size comparable to that of equivalent C programs.
In the table below, lrs + musl
denotes programs that were statically compiled
against musl, and lrs - libc
denotes programs that don’t depend on a libc.
Name | lrs + glibc | lrs + musl | lrs - libc | C (glibc) | rust |
---|---|---|---|---|---|
Hello World |
7.0KB |
4.0KB |
1.3KB |
6.5KB |
436KB |
18KB |
21KB |
n/a |
35KB |
462KB |
|
9.2KB |
5.8KB |
n/a |
n/a |
437KB |
Note
|
All programs were compiled with the -O -C lto flags.
|
lrs interacts with the kernel directly through system calls. That is, lrs does not depend on a libc for 99% of the work. This allows us to use kernel features that do not (yet) have an equivalent libc function and removes an unnecessary layer of abstraction.
It is, in fact, possible to use lrs without a libc. However, this mode is not yet fully developed and mostly useful for testing cross-compilation.
Due to what was discussed in the previous section, the lrs/libc interface is so small that it’s almost trivial to make lrs work with different libc versions. Currently, lrs is known to work with glibc and musl.
Even though lrs avoids the libc abstraction layer, porting lrs to new linux platforms is easy. This is due to the way the platform dependent parts of lrs mirror the equivalent parts in the linux kernel source code. lrs has already been ported to x86_64, x32, i686, arm, and arm64.
Warning
|
lrs has only been tested on x86_64. |
Many allocating structures in lrs (such as vectors, strings, hashmaps) come with an optional allocator argument. The following allocators are part of lrs:
- Libc
-
Uses
malloc
and friends from the libc. - JeMalloc
-
Uses jemalloc’s non-standard API with sized allocations and deallocations for higher efficiency.
- NoMem
-
This dummy-allocator always reports an out-of-memory condition.
- Bda
-
The brain-dead allocator only allocates in multiples of the page size. This is very useful for applications that have few allocations whose size is unknown at compile time and can rapidly increase.
Careful note should be taken of the NoMem allocator. Consider the following code:
let mut buf = [0; 20];
let mut vec = Vec::buffered(&mut buf);
write!(&mut vec, "Hello World {}", 10).unwrap();
assert!(&*vec == "Hello World 10");
The vector is backed by the NoMem allocator and the buffer declared in the
first line. It will never dynamically allocate any memory. If we were to write
more bytes than can be stored in the buffer, write!
would return that the
vector is out of memory. Using this feature, lrs often allows the user to avoid
allocations in cases where doing so would be rather inconvenient in rust.
Nevertheless, it’s easy to use lrs collections in the common case where the user
does not care about dynamic allocations. This is because all collections declare
a default allocator so that Vec<T>
is the same as Vec<T, Heap>
. This default
allocator can be chosen at compile time.
All APIs are designed to not allocate memory in the common case. For example,
File::open
will only allocate memory if the requested path is longer than
PATH_MAX
. In those cases the API uses the so called fallback allocator. If
the user does not want memory to be allocated in those exceptional situations,
he can disable said allocator at compile time.
lrs is split into many small crates and provides incremental compilation independent of the rustc compiler. Compiling a single crate during development often takes less than a second. To this end, lrs comes with its own build system—lrs_build—which ensures that only the minimal amount of work is done by the compiler.
Furthermore, even complete builds do not take very long. On this (old) machine, a complete build takes 28 seconds without optimization and 41 seconds with optimization.
lrs already wraps many of the commonly used linux system calls. See this page of the lrs documentation for a list of mostly safe system call wrappers. But note that most of these functions have much nicer wrappers in other parts of the library.
For example, the File struct exposes many syscalls that modify files.
Even though lrs programs don’t use the standard library that comes with the compiler, the user doesn’t have to bother with annoying annotations. For example, the following lrs program can be compiled as written:
use std::tty::{is_a_tty};
fn main() {
if is_a_tty(&1) {
println!("stdout is a tty");
} else {
println!("stdout is not a tty");
}
}
This is because lrs comes with its own compiler driver that takes care of injecting lrs instead of rust.
Please see the detailed Building and Using guide.
Documentation regarding lrs in general can be found in the Documentation directory.
The whole library is licensed under the MPL 2.0 license which allows static linking into proprietary programs. It is copy-left on a file-by-file basis: Changes to files licensed under the MPL 2.0 have to be distributed under the same license. It also allows the code to be freely used under several (L)GPL licenses.
Some other parts—such as the compiler plugin and the compiler driver—are licensed under the MIT license.
The lrs logo shows a penguin in a sprocket.
It is based on Simple Linux Logo by Dablim and is licensed under CC BY-SA 4.0.