Rust is not a simple language. As with any such language, it has many little tidbits of complexity that most folks aren’t aware of. Many of these tidbits are ones which may not practically matter much for everyday Rust programming, but are interesting to know. Others may be more useful. I’ve found that a lot of these aren’t documented anywhere (not that they always should be), and sometimes depend on knowledge of compiler internals or history. As a fan of programming trivia myself, I’ve decided to try writing about these things whenever I come across them. “Tribal Knowledge” shouldn’t be a thing in a programming community; and trivia is fun!

Previously in tidbits: Box is Special

Last time I talked about Box<T> and how it is a special snowflake. Corey asked that I write more about lang items, which are basically all of the special snowflakes in the stdlib.

So what is a lang item? Lang items are a way for the stdlib (and libcore) to define types, traits, functions, and other items which the compiler needs to know about.

For example, when you write x + y , the compiler will effectively desugar that into Add::add(x, y) 1. How did it know what trait to call? Did it just insert a call to ::core::Add::add and hope the trait was defined there? This is what C++ does; the Itanium ABI spec expects functions of certain names to just exist, which the compiler is supposed to call in various cases. The __cxa_guard_* functions from C++s deferred-initialization local statics (which I’ve explored in the past) are an example of this. You’ll find that the spec is full of similar __cxa functions. While the spec just expects certain types, e.g. std::type_traits (“Type properties” § 20.10.4.3), to be magic and exist in certain locations, the compilers seem to implement them using intrinsics like __is_trivial<T> which aren’t defined in C++ code at all. So C++ compilers have a mix of solutions here, they partly insert calls to known ABI functions, and they partly implement “special” types via intrinsics which are detected and magicked when the compiler comes across them.

However, this is not Rust’s solution. It does not care what the Add trait is named or where it is placed. Instead, it knew where the trait for addition was located because we told it. When you put #[lang = "add"] on a trait, the compiler knows to call YourTrait::add(x, y) when it encounters the addition operator. Of course, usually the compiler will already have been told about such a trait since libcore is usually the first library in the pipeline. If you want to actually use this, you need to replace libcore.

Huh? You can’t do that, can you?

It’s not a big secret that you can compile rust without the stdlib using #![no_std] . This is useful in cases when you are on an embedded system and can’t rely on an allocator existing. It’s also useful for writing your own alternate stdlib, though that’s not something folks do often. Of course, libstd itself uses #![no_std] , because without it the compiler will happily inject an extern crate std while trying to compile libstd and the universe will implode.

What’s less known is that you can do the same thing with libcore, via #![no_core] . And, of course, libcore uses it to avoid the cyclic dependency. Unlike #![no_std] , no_core is a nightly-only feature that we may never stabilize2. #![no_core] is something that’s basically only to be used if you are libcore (or you are an alternate Rust stdlib/core implementation trying to emulate it).

Still, it’s possible to write a working Rust binary in no_core mode:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # ! [ feature ( no_core )] # ! [ feature ( lang_items )] // Look at me. // Look at me. // I'm the libcore now. # ! [ no_core ] // Tell the compiler to link to appropriate runtime libs // (This way I don't have to specify `-l` flags explicitly) #[cfg(target_os = "linux" )] #[link(name = "c" )] extern {} #[cfg(target_os = "macos" )] #[link(name = "System" )] extern {} // Compiler needs these to proceed #[lang = "sized" ] pub trait Sized {} #[lang = "copy" ] pub trait Copy {} // `main` isn't the actual entry point, `start` is. #[lang = "start" ] fn start ( _main : * const u8 , _argc : isize , _argv : * const * const u8 ) -> isize { // we can't really do much in this benighted hellhole of // an environment without bringing in more libraries. // We can make syscalls, segfault, and set the exit code. // To be sure that this actually ran, let's set the exit code. 42 } // still need a main unless we want to use `#![no_main]` // won't actually get called; `start()` is supposed to call it fn main () {}

If you run this, the program will exit with exit code 42.

Note that this already adds two lang items. Sized and Copy . It’s usually worth looking at the lang item in libcore and copying it over unless you want to make tweaks. Beware that tweaks may not always work; not only does the compiler expect the lang item to exist, it expects it to make sense. There are properties of the lang item that it assumes are true, and failure to provide an appropriate lang item may cause the compiler to assert without a useful error message. In this case I do have a tweak, since the original definition of Copy is pub trait Copy: Clone {} , but I know that this tweak will work.

Lang items are usually only required when you do an operation which needs them. There are 72 non- deprecated lang items and we only had to define three of them here. “start” is necessary to, well, start executables, and Copy / Sized are very crucial to how the compiler reasons about types and must exist.

But let’s try doing something that will trigger a lang item to be required:

1 pub static X : u8 = 1 ;

Rust will immediately complain:

1 2 $ rustc test . rs error : requires ` sync ` lang_item

This is because Rust wants to enforce that types in statics (which can be accessed concurrently) are safe when accessed concurrently, i.e., they implement Sync . We haven’t defined Sync yet, so Rust doesn’t know how to enforce this restruction. The Sync trait is defined with the “sync” lang item, so we need to do:

1 2 3 4 5 pub static X : u8 = 1 ; #[lang = "sync" ] pub unsafe trait Sync {} unsafe impl Sync for u8 {}

Note that the trait doesn’t have to be called Sync here, any trait name would work. This definition is also a slight departure from the one in the stdlib, and in general you should include the auto trait impl (instead of specifically using unsafe impl Sync for u8 {} ) since the compiler may assume it exists. Our code is small enough for this to not matter.

Alright, let’s try defining our own addition trait as before. First, let’s see what happens if we try to add a struct when addition isn’t defined:

1 2 3 4 5 struct Foo ; #[lang = "start" ] fn start ( _main : * const u8 , _argc : isize , _argv : * const * const u8 ) -> isize { Foo + Foo }

We get an error:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ rustc test . rs error [ E0369 ] : binary operation ` + ` cannot be applied to type ` Foo ` --> test . rs : 33 : 5 | 33 | Foo + Foo | ^^^ | note : an implementation of ` std :: ops :: Add ` might be missing for ` Foo ` --> test . rs : 33 : 5 | 33 | Foo + Foo | ^^^ error : aborting due to previous error

It is interesting to note that here the compiler did refer to Add by its path. This is because the diagnostics in the compiler are free to assume that libcore exists. However, the actual error just noted that it doesn’t know how to add two Foo s. But we can tell it how!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #[lang = "add" ] trait MyAdd < RHS > { type Output ; fn add ( self , other : RHS ) -> Self :: Output ; } impl MyAdd < Foo > for Foo { type Output = isize ; fn add ( self , other : Foo ) -> isize { return 42 ; } } struct Foo ; #[lang = "start" ] fn start ( _main : * const u8 , _argc : isize , _argv : * const * const u8 ) -> isize { Foo + Foo }

This will compile fine and the exit code of the program will be 42.

An interesting bit of behavior is what happens if we try to add two numbers. It will give us the same kind of error, even though the addition of concrete primitives doesn’t go through Add::add (Rust asks LLVM to generate an add instruction directly). However, any addition operation still checks if Add::add is implemented, even though it won’t get used in the case of a primitive. We can even verify this!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #[lang = "add" ] trait MyAdd < RHS > { type Output ; fn add ( self , other : RHS ) -> Self :: Output ; } impl MyAdd < isize > for isize { type Output = isize ; fn add ( self , other : isize ) -> isize { self + other + 50 } } struct Foo ; #[lang = "start" ] fn start ( _main : * const u8 , _argc : isize , _argv : * const * const u8 ) -> isize { 40 + 2 }

This will need to be compiled with -C opt-level=2 , since numeric addition in debug mode panics on wrap and we haven’t defined the "panic" lang item to teach the compiler how to panic.

It will exit with 42, not 92, since while the Add implementation is required for this to type check, it doesn’t actually get used.

So what lang items are there, and why are they lang items? There’s a big list in the compiler. Let’s go through them:

The ImplItem ones (core) are used to mark implementations on primitive types. char has some methods, and someone has to say impl char to define them. But coherence only allows us to impl methods on types defined in our own crate, and char isn’t defined … in any crate, so how do we add methods to it? #[lang = "char"] provides an escape hatch; applying that to impl char will allow you to break the coherence rules and add methods, as is done in the standard library. Since lang items can only be defined once, only a single crate gets the honor of adding methods to char , so we don’t have any of the issues that arise from sidestepping coherence.

There are a bunch for the marker traits (core):

Send is a lang item because you are allowed to use it in a + bound in a trait object ( Box<SomeTrait+Send+Sync> ), and the compiler caches it aggressively

is a lang item because you are allowed to use it in a bound in a trait object ( ), and the compiler caches it aggressively Sync is a lang item for the same reasons as Send , but also because the compiler needs to enforce its implementation on types used in statics

is a lang item for the same reasons as , but also because the compiler needs to enforce its implementation on types used in statics Copy is fundamental to classifying values and reasoning about moves/etc, so it needs to be a lang item

is fundamental to classifying values and reasoning about moves/etc, so it needs to be a lang item Sized is also fundamental to reasoning about which values may exist on the stack. It is also magically included as a bound on generic parameters unless excluded with ?Sized

is also fundamental to reasoning about which values may exist on the stack. It is also magically included as a bound on generic parameters unless excluded with Unsize is implemented automatically on types using a specific set of rules (listed in the nomicon). Unlike Send and Sync , this mechanism for autoimplementation is tailored for the use case of Unsize and can’t be reused on user-defined marker traits.

Drop is a lang item (core) because the compiler needs to know which types have destructors, and how to call these destructors.

CoerceUnsized is a lang item (core) because the compiler is allowed to perform DST coercions (nomicon) when it is implemented.

All of the builtin operators (also Deref and PartialEq / PartialOrd , which are listed later in the file) (core) are lang items because the compiler needs to know what trait to require (and call) when it comes across such an operation.

UnsafeCell is a lang item (core) because it has very special semantics; it prevents certain optimizations. Specifically, Rust is allowed to reorder reads/writes to &mut foo with the assumption that the local variable holding the reference is the only alias allowed to read from or write to the data, and it is allowed to reorder reads from &foo assuming that no other alias writes to it. We tell LLVM that these types are noalias . UnsafeCell<T> turns this optimization off, allowing writes to &UnsafeCell<T> references. This is used in the implementation of interior mutability types like Cell<T> , RefCell<T> , and Mutex<T> .

The Fn traits (core) are used in dispatching function calls, and can be specified with special syntax sugar, so they need to be lang items. They also get autoimplemented on closures.

The "str_eq" lang item is outdated. It used to specify how to check the equality of a string value against a literal string pattern in a match ( match uses structural equality, not PartialEq::eq ), however I believe this behavior is now hardcoded in the compiler.

The panic-related lang items (core) exist because rustc itself inserts panics in a few places. The first one, "panic" , is used for integer overflow panics in debug mode, and "panic_bounds_check" is used for out of bounds indexing panics on slices. The last one, "panic_fmt" hooks into a function defined later in libstd.

The "exchange_malloc" and "box_free" (alloc) are for telling the compiler which functions to call in case it needs to do a malloc() or free() . These are used when constructing Box<T> via placement box syntax and when moving out of a deref of a box.

"strdup_uniq" seemed to be used in the past for moving string literals to the heap, but is no longer used.

We’ve already seen the start lang item (std) being used in our minimal example program. This function is basically where you find Rust’s “runtime”: it gets called with a pointer to main and the command line arguments, it sets up the “runtime”, calls main, and tears down anything it needs to. Rust has a C-like minimal runtime, so the actual libstd definition doesn’t do much. But you theoretically could stick a very heavy runtime initialization routine here.

The exception handling lang items (panic_unwind, in multiple platform-specific modules) specify various bits of the exception handling behavior. These hooks are called during various steps of unwinding: eh_personality is called when determining whether or not to stop at a stack frame or unwind up to the next one. eh_unwind_resume is the routine called when the unwinding code wishes to resume unwinding after calling destructors in a landing pad. msvc_try_filter defines some parameter that MSVC needs in its unwinding code. I don’t understand it, and apparently, neither does the person who wrote it.

The "owned_box" (alloc) lang item tells the compiler which type is the Box type. In my previous post I covered how Box is special; this lang item is how the compiler finds impls on Box and knows what the type is. Unlike the other primitives, Box doesn’t actually have a type name (like bool ) that can be used if you’re writing libcore or libstd. This lang item gives Box a type name that can be used to refer to it. (It also defines some, but not all, of the semantics of Box<T> )

The "phantom_data" (core) type itself is allowed to have an unused type parameter, and it can be used to help fix the variance and drop behavior of a generic type. More on this in the nomicon.

The "non_zero" lang item (core) marks the NonZero<T> type, a type which is guaranteed to never contain a bit pattern of only zeroes. This is used inside things like Rc<T> and Box<T> – we know that the pointers in these can/should never be null, so they contain a NonZero<*const T> . When used inside an enum like Option<Rc<T>> , the discriminant (the “tag” value that distinguishes between Some and None ) is no longer necessary, since we can mark the None case as the case where the bits occupied by NonZero in the Some case are zero. Beware, this optimization also applies to C-like enums that don’t have a variant corresponding to a discriminant value of zero (unless they are #[repr(C)] )

There are also a bunch of deprecated lang items there. For example, NoCopy used to be a struct that could be dropped within a type to make it not implement Copy ; in the past Copy implementations were automatic like Send and Sync are today. NoCopy was the way to opt out. There also used to be NoSend and NoSync . CovariantType / CovariantLifetime /etc were the predecessors of PhantomData ; they could be used to specify variance relations of a type with its type or lifetime parameters, but you can now do this with providing the right PhantomData , e.g. InvariantType<T> is now PhantomData<Cell<T>> . The nomicon has more on variance. I don’t know why these lang items haven’t been removed (they don’t work anymore anyway); the only consumer of them is libcore so “deprecating” them seems unnecessary. It’s probably an oversight.

Interestingly, Iterator and IntoIterator are not lang items, even though they are used in for loops. Instead, the compiler inserts hardcoded calls to ::std::iter::IntoIterator::into_iter and ::std::iter::Iterator::next , and a hardcoded reference to ::std::option::Option (The paths use core in no_std mode). This is probably because the compiler desugars for loops before type resolution is done, so withut this, libcore would not be able to use for loops since the compiler wouldn’t know what calls to insert in place of the loops while compiling.

Basically, whenever the compiler needs to use special treatment with an item – whether it be dispatching calls to functions and trait methods in various situations, conferring special semantics to types/traits, or requiring traits to be implemented, the type will be defined in the standard library (libstd, libcore, or one of the crates behind the libstd façade), and marked as a lang item.

Some of the lang items are useful/necessary when working without libstd. Most only come into play if you want to replace libcore, which is a pretty niche thing to do, and knowing about them is rarely useful outside of the realm of compiler hacking.