Why doesn't rust adopt swift-style import?

I still miss the C-style compilation model: each source file is compiled into an obj file, and each obj is the smallest unit for linking. The advantages of this model include:

  1. The target exe or dll has a size as small as possible.
  2. Each file can be compiled independently, without requiring to have all its dependencies available beforehand.

The disadvantages of this model include:

  1. It requires header files, which is considered not modern.

But how do swift, java, c#, go and other modern programming languages deal with this problem? I never heard that swift has a concept like crate in rust, and I also never heard swift requires a header file. Is the concept of crate REALLY necessary to achieve the benefits of rust: safety, concurrency, high performance? What stops us from adopting a swift-style import?

You probably want to google that question but from what I can find it looks like Java uses a similar model to C/C++ and I'm pretty sure Go uses a package (their equivalent of a crate) as their compilation unit.

C# builds into "assemblies", which are very similar to crates. C#'s internal is Rust's pub(crate). It builds a bunch of files at the same time into a single unit, and that unit is what other units reference. Etc.

As explained in a previous thread, your first assumption is incorrect:

  1. The target exe or dll has a size as small as possible.

This is not true, because the linker doesn't link crates. It links sections, and each crate is made of as many sections as there are functions. For purpose of linking and binary size, functions are Rust's smallest compilation unit.

Usual C executables dynamically link to a 10MB+ large libc that is installed on every system. Because Rust can't expect Rust's stdlib to be installed on everyone's systems, Rust has to copy its stdlib (only parts you reference) into executables. Swift & Go have the same problem, and produce binaries about as large as Rust.

To make them as small as possible:

  • Disable jemalloc, use alloc_system (uses C's allocator. Saves 300KB, may cost performance if C's allocator is slower)
  • Use strip (removes 2MB of debug info on Linux)
  • Use LTO (merges all crates from the entire program into one compilation unit, which allows removal of unused code to be more aggressive)
8 Likes

I don't quite understand this quote:

Is it convenient for you to elaborate it further?

By the way, Michael-F-Bryan seems to hold another opinion. Anyway, I need to brush up my knowledge in library and linking.

I don't believe so. Reading through @kornel's post I'd say we roughly agree, we just use different words and analogies to explain it.

The whole idea of "compilation units" becomes quite fuzzy when you bring in ideas like dynamic/static linking, the linker, and optimisations such as dead code elimination.

In a very hand wavey way, you could say the C model cheats.

It doesn't actually produce smaller binaries, instead it appears to make smaller binaries because it will dynamically link to the system libc. This means they can have a massive libc library with the executable itself being tiny that just calls out to code from libc. You can get away with this because every operating system will always have a copy of libc present, so you never have to worry about your program not being able to start because it can't find libc.

Rust isn't nearly as ubiquitous as C and therefore there is no guarantee std (Rust's equivalent of libc) will be available on the machine running a program (remember that you don't always run a program on the same machine it was compiled on). Therefore the Rust compiler bundles the bits of std an executable will need (and just the bits it will need, stripping out the unused stuff) inside the binary.

10/10 would recommend. The entire topic is super interesting and once you get a better understanding of how things work under the hood you'll probably be able to look back at these questions and answer themselves :slight_smile:

1 Like

Ahem.

This does bring up another reason that having a statically linked standard library is useful; a reduced reliance on the environment that the executable is running in. Since Rust doesn't yet have a stable ABI you would have to have the exact version of std that an executable was compiled against to run it. In the future once Rust has a stable ABI and if the velocity of changes to std ever reduce it may make sense to start distributing programs dynamically linked to std; but from what I've seen that would be a minimum of multiple years away, and I wouldn't be surprised if it takes near a decade.

2 Likes

When you compile printf("Hello World!\n"); in C, the executable does not contain the printf function, and is unable to print anything by itself. Instead, the C executable loads a libc library already installed on your system and tells that library to print the string.

When you compile println!("Hello World"); in Rust, the executable does contain the println! code and everything that it requires to print the string itself.

It is this way, because every major operating system has printf built-in, but doesn't have println! built-in.

For example on macOS executables that use malloc() load it from /usr/lib/system/libsystem_malloc.dylib which is 200KB large. Rust can use it too, or can use own jemalloc. But there's no /usr/lib/system/jemalloc.dylib, so every Rust executable with jemalloc has to bundle its own copy.

6 Likes