The experience from a few years ago: "Then we hit a wall with the dynamic linker. At the time you could only link Swift libraries dynamically. Unfortunately the linker executed in polynomial time so Apple’s recommend maximum number of libraries in a single binary was 6. We had 92 and counting. As a result It took 8-12 seconds after tapping the app icon before main was even called. Our shinny new app was slower than the old clunky one. Then the binary size problem hit." - https://twitter.com/StanTwinB/status/1336890442768547845
The twitter link doesn't work with js disabled unless you turn your browser into a bot via user agent switching.
But the thereaderapp link works perfectly fine without any js while simultaneously stripping out all the other garbage inherent on twitter like advertising, "discussion", or "related" content.
It's rare to read a corporate story where a company traps itself with technical choices so thoroughly, and still manages to pull through in the end. Much respect for the Uber engineers.
I wouldn’t have had the patience to stick with it and I’m a game developer. It almost seems like it would’ve been faster to build their own compiler of swift. I’m only half joking.
To be extra clear about this, Rust does support dynamic linking; it's just limited to interfacing via the stable C ABI. The ABI-resilience conerns described in the article are ultimately punted to the users and/or the dev ecosystem, albeit some facilities (namely, bindgen/cbindgen) are also provided to ease the task.
With a big caveat: you can't have a dependency on a crate that is a dylib/cdylib or even staticlib in Cargo.toml. Your only way out is to have a build.rs that somehow builds the dependency with cargo. Which, if you're using a workspace, will deadlock. https://github.com/rust-lang/cargo/issues/8938
technically speaking, you can dynamic link regardless of the abi or not, using a stable ABI just provides you degrees of freedom with which compiler version compiles what. They're orthogonal concepts, even if they're most useful when combined.
Yes, the same is true of e.g. GHC the Haskell compiler. There's nothing stopping you from using dynamic linking with GHC on Haskell code (i.e. your haskell app links to a haskell shared object), but if the language ABI isn't stable, you just rebuild all those shared objects with every new version of the compiler anyway. In fact you don't even need to use a different compiler version: aggressive cross-module inlining also reduces the amount of sharing you can do (even, potentially, down to zero sharing.)
So you rarely save disk and every app has different shared objects loaded, and even assuming you use one compiler for everything, it may not matter at all. If anything it's worse because it can negatively impact startup time. In practice, ~everyone compiles Haskell code statically, and only dynamically links to libraries that have exposed a stable ABI. I'm not surprised people do the same for Rust.
It seems like the fundamental issue is tradeoffs between compile-time and run-time efficiency. During development in almost every compiled language, it would make sense to have minimal cached, shared object (or shared intermediate) code in most use-cases while globally compiling and optimizing for releases. Invoking every run-time optimization and recompiling everything during development or first package installation makes zero sense because it's a waste of time.
Also, shared common package library units for releases by default shrinks binaries rather than having zillions of exhaustive, slightly-different combinations of similar structures and machine code hoarding space in binaries. I've never understood how 300 MiB binaries would ever be acceptable to anyone, especially going against decades of shared libraries that reduced binary sizes and memory usage. Deliberate waste is never acceptable because there is no free lunch.
Usually when a language supports dynamic loading we refer to the language itself, as integration with OS ABI dynamic loader is a given for any language worth using.
There is a lot of great tech behind Swift and LLVM. I really enjoyed learning a bit (no pun intended) of Rust a few years ago, but it didn't stick for what I need. I really like Swift, but I have only done one SwiftUI app, all the Swift stuff I do is on the command line with swift build/test/run with a light weight editor. Apple's pre-trained deep learning models are very useful, and an edit/build/run iteration is just a second or two.
I wish Rust had been around 30 years ago when I loved doing low level programming. Between my C++ books and gigs, C++ was good financially but Rust is just better (apologies to people who love C++).
While many of the ideas in Rust are not particularly radical in terms of compiler research, I think part of its success is because it had the past 30 years of lessons. We all stand on the shoulders of giants. Maybe in another 30 years we'll see a new language that learns from Rust's inevitable mistakes.
> I wish Rust had been around 30 years ago when I loved doing low level programming.
I think Rust’s killer feature is lifetime analysis by the compiler. In 1991, you had 33 MHz 80486 with around 4 MB of RAM. I don’t think the average developer had the computing power to do Rust’s lifetime in any reasonable amount of time.
Lifetime analysis doesn’t even show up in Rust compiler profiles. It takes 0 seconds.
Rust lifetime analysis is a function-local pass. It never needs to access any state outside the function. It can be done on all functions in parallel, etc.
To be honest, I am actually glad about this. Dynamic linking is the cause of a lot of complexity. First there is DLL hell on Windows and its equivalent on Linux. Second, having to maintain a stable ABI is a huge drain on language evolution. In C++, there have been many features that have been vetoed by the compiler/standard library implementers because it would cause an ABI break. Because of this ABI concerns, C++ is stuck with overheads for unique_ptr, map, unordered_map, and regex classes that are inefficient.
Secondly, a lot of the benefits of dynamic linking don’t make as much sense now.
First with regards to space savings. With modern disk capacity having extra copies of executable code is probably not that big of a deal. In addition with Link Time Optimization, you actually may not be saving space with dynamic linking since the linker can pretty aggressively throw away code that it knows is ever called.
Second, with regards to security. One thing to consider is that dynamic linking is so complex that it is one of the reasons why people use docker, to make sure their binary and its dependencies are stable. Once you have something in a docker, it has a lot of the same update issues as a static linked binary, only less efficient.
In addition, with security, Rust unlike C and C++ is a memory safe language. Given the vast majority of security issues are memory safety issues, I would expect Rust to have a lot fewer CVE’s. I think the experience with Go is likely enlightening in this regard. I am not aware of a huge issue of lack of security because it didn’t do dynamic linking and you couldn’t do security updates as easily.
Finally, Rust as a new language is embracing the new way of programming and deploying. We are shifting away from using binary artifacts (often closed source) that once built were rarely changed, to build from source and continuously build/deploy. Cargo makes it relatively easy to do this. In that model, updating a binary for security is just a subset of the normal building and updating of a binary that is done daily.
By avoiding dynamic linking, I think, Rust positions itself best as the no-compromise, high performance, modern, systems programming language.
Rust is certainly made simpler by not sweating ABI stability. But the main advantage of dynamic linking is to enable yesterday's app to run on tomorrow's OS (and vice-versa). It's why your apps keep working when you update your phone.
Without a dynamic linking story, Rust is just not viable for writing UIKit, Android's frameworks, etc. Which is OK, it's a reasonable choice, but it limits Rust's scope.
Zig is in a really good position here. Its featureset sticks closer to C, and being able to automatically generate C headers from Zig code and automatically consume C headers from Zig code is really convenient.
Does Zig really solve this problem? For example, if I add a field to a Zig struct, can existing code still use the struct without needing to be recompiled?
First of all, C/C++ can do static linking as well as dynamic linking. If static linking is preferred, people will do it. Second, dynamic linking is necessary because of how modern OSs are designed. Most GUIs are gigantic libraries that cannot be statically linked. Similarly for network code and other areas of modern OSs.
I tend to agree. Dynamic linking is only a win on space when 1) you have multiple programs running which share the same library, 2) they're not all identical programs which would share code, and 3) the programs use a significant fraction of what's in each dynamic library they load.
With static linking, you get to prune at the function level at link time. DLLs can't do that.
A major use of dynamic linking is loading extensions. Without DL you'll have a hard time using languages such as Python, or creating extensions to applications.
Dynamic linking and dynamic loading aren't necessarily the same thing, are they? I imagine one could statically link in some C code to a rust program that uses dlopen, for example, or even just do the syscall directly in rust (assuming that's possible).
The difference, I think, is that dynamic linking necessarily binds local symbols to the dynamically loaded foreign code, while dynamic loading simply loads the foreign code into the same address space and provides an API for looking up symbols' addresses by name. There's no inherent need for the ABI to match, as long as you don't directly try to call into a function; you can write adapters to explicitly handle the dynamically loaded code's ABI, like python does with ctypes.
At the end of the day it's all machine code running on silicon. What point are you trying to make? I was responding to the claim that a lack of good support for dynamic linking in a language prevents it from using shared objects for plugins and modules. My point was that dynamic loading is an operating system feature implemented through system calls, and can therefore be done in any language that can do a system call, whether or not the language makes it easy for you. I also claim that dynamic linking is not equivalent to loading, but I agree with you that they are related to each other and linking is indeed implemented on top of the system calls that provide dynamic loading. Dynamic linking specifically implies (for me) that non-static application symbols are resolved one way or another before the program counter jumps to main().
First of all, depending on the OS there are no syscalls to call to, Linux is the exception here.
Everyone else has to go through dlopen, LoadLibrary, or whatever alternative the OS offers.
Which is exactly the same code path that at some level the OS dynamic linker relies on.
If you are statically linking everything, then there are no libraries to give input to dlopen anyway, it doesn't load .a files, and .so is not what we want to support anyway.
But lets keep the assumption that in the world of static linking advocacy, explicitly loading dynamic libraries is acceptable.
SO now, not only has the application to do the job of mapping dynamic library symbols to function pointers, they are constrained to C ABI function calls, which is not Rust anyway.
As for you Linux based assumption, there are OSes, where dynamic linking doesn't take place before the PC jumps to main, you can do deferred dynamic linking. The call sites are marked for triggering a page fault on call, which only when it happens, will the OS look around for code to load.
This is how managed languages like Java and C# work, which many keep forgetting there are also AOT compilers, just in case the VM arguments comes into play.
But since we are talking about systems languages, you can find this feature on Windows and Aix, just to give two examples how the OS world isn't everywhere a Linux clone.
Just dropping by to say that you're right. You can absolutely _load_ dynamic libraries from Rust. I've done this. But of course, the executable is not dynamically _linked_ to the requested libraries. E.g., running `ldd` on my executable won't show which shared libraries it is or should be linked against. Rather, it will just fail with an error if the requested library is not there. There are crates for loading dynamic libraries that use `dlopen` under the hood.
In abstract you could separate these things, but in modern OSs there is little to no difference between these concepts. Dynamic loading is implemented using dynamic linking and if you don't want to use dynamic linking then you have to reinvent the wheel down to the OS level. You will have a very hard time to make dynamic loading available in any architecture following this route.
Neat, memory mapping is a nifty concept. Are you loading a raw binary blob and directly jumping into it then? That's hardcore.
I'm not familiar with the inner workings of dlopen, but I wouldn't be surprised if dlopen itself is mostly userspace code that knows how to interpret the ELF format, and internally it uses mmap as the actual system call. Maybe it has its own system call, though, to let the kernel do most of the work?
I've seen some really cool uses of mmap. It's nice for reducing the number of memory copies when you're churning through a bunch of data on disk, and often necessary when using shared memory for low overhead inter-process communication. I worked on a project that needed publish-subscribe message passing semantics with fine-grained synchronization across dozens of processes, for example, and a lock-free shared memory "disrupter" queue structure was able to handle millions of messages per second, while sockets could only handle a few hundred thousand or so.
I've seen some unfortunate uses as well. Sometimes folk like the idea of manipulating files on disk as arrays in memory, and use mmap even though there's no practical performance need for it. The code often times ends up harder to read than it would have had it used file descriptors. Also, the performance ends up worse because the implemented algorithm's access patterns unknowingly cause lots of page faults and disk seeks because the data is an "array". Often times there's an alternative way to do the same work sequentially with an additional in-memory data structure, and using file descriptors naturally biases one towards doing that.
Works fine on BSD (though it's been long enough that I forget which BSD) as long as you ignore the parts of the documentation written by facist morons. Haven't had occasion to beat proper linking behaviour into a Windows machine, but what I've heard suggests it'd be a pain the ass, yes. (Then again, Windows isn't a OS, it's a shitty GUI for DOS that bloated until it could read email. Kinda like OSX, except there isn't a vaguely decent OS buried under the manure.)
A systems programming language cannot afford to silo itself on a single OS solutions.
There are plenty of OSes to choose from by the way, even your rant barely scratches the surface of the options available for any serious systems programming language.
> A systems programming language cannot afford to silo itself on a single OS solutions.
If a OS (or alleged OS) demands that you dynamically link against libc (or anything else written in C) to interact with it, that problem is not the (non-C) programming language's fault.
> your rant barely scratches the surface of the [OS] options available
That's fair, but most of the not-linux I have experience with is microcontrollers or other things that are far enough from unix/posix that they don't have dlopen (mmap itself is hit-or-miss, but tends to be hit in the sorts of situations where I'd have occasion to dynamically load stuff in the first place), so there's not much I could do about that.
> has a lot of the same update issues as a static linked binary, only less efficient.
I think this is an insightful comment. Obviously there are other reasons to use docker, but too often (cough...Python...cough) I see virtualization used for exactly that.
>. One thing to consider is that dynamic linking is so complex that it is one of the reasons why people use docker, to make sure their binary and its dependencies are stable.
Coding for almost 40 years and I am yet to use Docker to sort out this kind of issues.
> I think the experience with Go is likely enlightening in this regard. I am not aware of a huge issue of lack of security because it didn’t do dynamic linking and you couldn’t do security updates as easily.
When I started programming, dynamic linking was only available on big iron machines filling computer rooms, we did not need Go for knowing what static linking entails.
> Finally, Rust as a new language is embracing the new way of programming and deploying. We are shifting away from using binary artifacts (often closed source) that once built were rarely changed, to build from source and continuously build/deploy. Cargo makes it relatively easy to do this. In that model, updating a binary for security is just a subset of the normal building and updating of a binary that is done daily.
If Rust wants to succeed in replacing C and C++ in typical big corp, cargo better support binary libraries eventually.
> By avoiding dynamic linking, I think, Rust positions itself best as the no-compromise, high performance, modern, systems programming language.
You can use static linking in c++ if you want to. There is 0 advantage in not having the option to use dynamic linking if you want to. It might be even more secure if e.g. the program links dynamically against a commonly used library and this library is updated regularly.
> You can use static linking in c++ if you want to. There is 0 advantage in not having the option to use dynamic linking if you want to.
While you can do static linking in C++ if you want to, you still pay the cost for support for dynamic linking because of ABI issues. You can static link, but you still lost out on new language features and changes that break ABI and so you are still stuck with sub-optimal standard library code. For example, for years, until GCC 5, GCC std lib had a non-standard, slower copy on write implementation of std::string, that stuck around for so long because it would break ABI to change it. Even now, in MSVC STL, there are a lot of performance optimizations which the maintainer acknowledges, but will have to wait because they would break ABI. So even if you just statically link and continuously build in C++, you are still paying the price for support of dynamic linking.
I largely agree and personally hate working with dynamic linking, but ultimately it depends on your architecture model - to share memory, or not to share memory. If you've already decided you don't want to share memory, and you don't want to use system IPC, that leaves you with sockets (network/unix/fifo), which are all just byte streams. If you have byte streams, then you gotta use (de)serialization (serde) on either end, so effectively there is one ABI: {uint8[], size_t}. This is the microservices/docker/cloud model, and it's great. For many purposes.
But if you are dealing with OS level stuff, talking directly to hardware, in the embedded space, etc, now you care about different ABIs for performance reason. Now you decide, do I want static linking or dynamic linking? The only place I see dynamic linking really being a feature is when you have to link against the kernel, drivers, or proprietary subsystems (e.g. CUDA). IMHO, most userspace application layer stuff shouldn't be sharing memory, it should be communicating, again using byte streams and serde.
So that basically leaves the hardware/software interface: kernel APIs, drivers, etc. If you're in this space, great, it makes sense to use dynamic linking with very stable ABIs with no generic shenanigans, you don't need them. I think a lot of pain of DLL hell comes from using dynamic libs when you really should be statically compiling if it's a library, and IPC if you need to communicate.
The GCC compiler for Java (GCJ) started out doing whole-program compilation, but that couldn't handle Java's flexible ABI and dynamic loading. In 2004 they introduced a compilation mode with an additional layer of indirection similar to Swift's: <ftp://gcc.gnu.org/pub/gcc/summit/2004/GCJ%20New%20ABI.pdf>. They didn't have to deal with monomorphizing, though.
If I've read this correctly, D has been able to dynamic link properly for years, against it's own ABI, the C ABI, or the C++ ABI (e.g. templates, vtables up to single inheritance)
Clickbait title aside, that post was really popular when it came out. I remember there was a period where it got reposted or mentioned on Rust Internals at least once every two weeks.
edit nvm I thought dynamic linking slowed things down since static linking means code fits better in the cache. Looking at the points, it seems that either dyanmic linking makes code faster or people prefer slower dynamic code.
From reading the first few pages I get triggered because the whole "where Rust couldn't" feels like mentioning Rust in the title to piggy back on Rust's rising popularity. I personally don't find Swift interesting in any way while Rust revolutionized GCless (and GCless parallelized) typed programming.
I'm one of the leads of the Rust language team, and I love seeing writeups like this. When we're designing Rust, we look carefully at other languages, to see examples of precedent, different trade-offs, and experiences; we don't design in a vacuum.
This is an extremely clear explanation of Swift making a different set of trade-offs to achieve a different goal, and it's worthy of consideration and evaluation. (I read it when it was first published.)
There are no attacks against Rust here. This is a professional write-up discussing two languages, by someone experienced with both.
Gankra worked on both Rust and Swift, and additionally wrote several very beloved Rust documentation resources, such as “Learning Rust With Entirely Too Many Linked Lists” and “The Rustinomicon.”
This is an extremely accomplished person writing about something she is possibly more qualified to than any other person on earth.
I agree that the author is extremely qualified to write about this, but personally I also find the title a little jarring, mostly due to the word "couldn't". It seems more that there are number of tradeoffs where allowing slight runtime costs by default (like reference counting) unlocks a lot of stuff that's needed to be able to make dynamic linking easier, and Rust chose to go a different route than Swift. I understand that changing the title to read "where Rust chose to optimize for different priorities" won't get as many clicks, but the title to me paints the lack of dynamic linking support in Rust as some sort of failing rather than an explicit design choice. Realistically, I think the fact that Rust and Swift picked different routes here is a good thing! Having languages which optimize for different things gives people more options to pick one that suits their needs better; in a world with both nails and screws, it's better to have both hammers and screwdrivers available.
I don't see the problem. Rust made some explicit choices that meant it couldn't do something. Doesn't change the fact that it couldn't do something. No need to get so defensive, lots of languages have things they can't do.
I totally agree. I have a problem only with the wording of the title and it shouldn't matter who wrote it. It's a matter of integrity and we as software engineers should set higher standards to how we write professional articles
Those were some of the most memorable sections for me. I never really thought about the other side of monomorphizing. It made it really obvious where "zero-cost abstractions" start to fall short.
Sure, that the author is an expert does not mean that she is infallible, or there's nothing to discuss. That's not what this sub-thread is about. This thread is about some sort of insinuation that this is born out of some sort of weird attempt at Swift to use Rust to gain prominence, or something.
Gankra was heavily involved in low level rust plumbing such as linking, ABIs, unsafe code, and so on. Comparing to rust is both speaking to the other similar language she has experience with, and speaking to many of the people who were likely to read this article (those people being from the rust community). This article also really does achieve the goal in the third paragraph (at least when read by the right audience)
> Also some folks like to complain that Rust doesn't bother with ABI stability, and I think looking at how Swift does helps elucidate why that is.
I care about Rust (mostly due to using it a lot), so I really hope more people spend the time to criticize it, and dig up the areas that need love and care compared to other languages.
But in this particular case, you should probably know that the author is not some nobody trying to piggyback on Rust's popularity :) They're a pretty well known contributor, in fact.
If by GCless you mean affine types, the only thing that Rust did was bringing affine types to the masses, which is already an achievement, agreed-
However in the big context of application programming, and how Swift is used on Apple stack, any form of garbage collection alongside memory ownership is much more productive.
Long term adoption for Rust will be places like where MISRA-C and SPARK are used nowadays.
If I have to spray my code with Arc, Rc and RefCell, I rather let the compiler type them for me.