I've worried about this for a while with Rust packages. The total size of a "big" Rust project's dependency graph is pretty similar to a lot of JS projects. E.g. Tauri, last I checked, introduces about 600 dependencies just on its own.
Like another commenter said, I do think it's partially just because dependency management is so easy in Rust compared to e.g. C or C++, but I also suspect that it has to do with the size of the standard library. Rust and JS are both famous for having minimal standard libraries, and what do you know, they tend to have crazy-deep dependency graphs. On the other hand, Python is famous for being "batteries included", and if you look at Python project dependency graphs, they're much less crazy than JS or Rust. E.g. even a higher-level framework like FastAPI, that itself depends on lower-level frameworks, has only a dozen or so dependencies. A Python app that I maintain for work, which has over 20 top-level dependencies, only expands to ~100 once those 20 are fully resolved. I really think a lot of it comes down to the standard library backstopping the most common things that everybody needs.
So maybe it would improve the situation to just expand the standard library a bit? Maybe this would be hiding the problem more than solving it, since all that code would still have to be maintained and would still be vulnerable to getting pwned, but other languages manage somehow.
I wouldn't call the Rust stdlib "small". "Limited" I could agree with.
On the topics it does cover, Rust's stdlib offers a lot. At least on the same level as Python, at times surpassing it. But because the stdlib isn't versioned it stays away from everything that isn't considered "settled", especially in matters where the best interface isn't clear yet. So no http library, no date handling, no helpers for writing macros, etc.
You can absolutely write pretty substantial zero-dependency rust if you stay away from the network and async
Whether that's a good tradeoff is an open question. None of the options look really great
It seems like the normal and rational choice to not include logging to me.
Java included 'java.util.logging'. No one uses it because it's bad, everyone just uses slf4j as the generic facade instead, and then some underlying implementation.
Golang included the "log" package, which was also a disaster. No one used it so they introduced log/slog recently, and still no one uses that one, everyone is still using zap, logrus, etc.
Golang is also a disaster in that the stdlib has "log/slog" as the approved logging method, but i.e. the 'http.Server.ErrorLog' is the old 'log.Logger' you're not supposed to use, and for backwards compatibility reasons it's stuck that way. The go stdlib is full of awkward warts because it maintains backwards compatibility, and also included a ton of extra knobs that they got wrong.
The argument of structured vs unstructured logging, arguments around log levels (glog style numeric levels, trace/debug/info/warn/error/critical or some subset, etc), how to handle lazy logging or lazy computations in logging, etc etc, it's all enough that the stdlib can't easily make a choice that will make everyone happy.
Rust as a language has provided enough that the community can make a sl4j like facade (conveniently, the clearly named 'log' crate seems to be the winner there), so there's no reason it has to be in the stdlib.
Python includes logging, and packages generally use it. It's far from perfect, but it's important to have at least a standard interface so that all components use it in concert. When you have a proliferation of logging libraries with no standard, other packages either don't do any logging at all, or do it in idiosyncratic ways that are hard (or impossible) to integrate.
> Golang included the "log" package, which was also a disaster. No one used it so they introduced log/slog recently, and still no one uses that one, everyone is still using zap, logrus, etc.
I honestly don't know how common it is to use zap, logrus and co on new projects anymore. I know I always go for slog.
> Golang is also a disaster in that the stdlib has "log/slog" as the approved logging method, but i.e. the 'http.Server.ErrorLog' is the old 'log.Logger' you're not supposed to use, and for backwards compatibility reasons it's stuck that way. The go stdlib is full of awkward warts because it maintains backwards compatibility, and also included a ton of extra knobs that they got wrong.
This isn't a practical problem at all. First of all, the stdlib shouldn't make backwards-incompatible changes just for the sake of logging. Second, slog.NewLogLogger()[0] exists as a way to get log.Loggers to be used with things that rely on the old log.Logger.
Rust's standard library hasn't received any major additions since 1.0 in 2015, back when nobody was writing web services in Rust so no one needed logging.
Clap went through some major redesigns with the 4.0 release just three years ago. That wouldn't have been possible if clap 2.0 or 3.0 had been added to the stdlib. It's almost a poster child for things where libraries where being outside the stdlib allows interface improvements (date/time handling would be the other obvious example).
Rand has the issue of platform support for securely seeding a secure rng, and having just an unsecure rng might cause people to use it when they really shouldn't. And serde is near-universal but has some very vocal opponents because it's such a heavy library. I have however often wished that num_traits would be in the stdlib, it really feels like something that belongs in there.
FWIW, there is an accepted proposal (https://github.com/rust-lang/libs-team/issues/394) to add random number generation to std, and adding traits like in `num-traits` is wanted, but blocked on inherent traits.
The std has stability promises, so it's prudent to not add things prematurely.
Go has the official "flag" package as part of the stdlib, and it's so absolutely terrible that everyone uses pflag, cobra, or urfave/cli instead.
Go's stdlib is a wonderful example of why you shouldn't add things willy-nilly to the stdlib since it's full of weird warts and things you simply shouldn't use.
> and it's so absolutely terrible that everyone uses pflag, ../
This is just social media speak for inconvenient in some cases. I have used flag package in lot of applications. It gets job done and I have had no problem with it.
> since it's full of weird warts and things you simply shouldn't use.
The only software that does not have problem is thats not written yet. This is the standard one should follow then.
That's pulling on a dependency. An ugly case, but a very specific one relating to a tricky backwards-incompatible change that the Go team fudged on back in the pre-module times.
That story doesn't generally speak to the reality that most Go packages have rather small dependency graphs.
Clap is enormous and seems way too clever for everything I do. Last I looked it added 10+ seconds to compile time and hundreds of kbs to binary size. Maybe something like ffmpeg requires that complexity, but if I am writing a CLI that takes three arguments, it is a heavy cost.
I honestly feel like that's one of Rust's biggest failings. In my ideal world libstd would be versioned, and done in such a way that different dependencies could call different versions of libstd, and all (sound/secure) versions would always be provided. E.g. reserve the "std" module prefix (and "core", and "alloc"), have `cargo new` default to adding the current std version in `cargo.toml`, have the prelude import that current std version, and make the module name explicitly versioned a la `std1::fs::File`, `std2::fs::File`. Then you'd be able to type `use std1::fs::File` like normal, but if you wanted a different version you could explicitly qualify it or add a different `use` statement. And older libraries would be using older versions, so no conflicts.
I'm afraid it won't work. The point of std lib is to be universal connection for all the libraries. But with versioned std I just can't see how can you have DateTime in std1, DateTime in std2 and use them interchangeably, for example being able to pass std2::DateTime to library depending on std1 etc. Maybe conversion methods, but it get really complicated really quickly
My personal experience (YMMV): Rust code takes 2x or 3x longer to write than what came before it (C in my case), but in the end you usually get something much more likely to work, so overall it's kind of a wash, and the product you get is better for customers - you basically front load the cost of development.
This is terrible for people working in commercial projects that are obsessed with time to market.
Rust developers on commercial projects are under incredible schedule pressure from day 0, where they are compared to expectations from their previous projects, and are strongly motivated to pull in anything and everything they can to save time, because re-rolling anything themselves is so damn expensive.
I think they were using "writing Rust" in the most strict sense: the part of the development cycle that involves typing the majority of the code, before you really start debugging in earnest and really make things work.
But their point is that "developing Rust" (as in, the entire process) ends up being a similar total effort to C, only with more up front "writing" and less work on the debugging phase.
Thank you for the clarification, that's exactly what I was trying to say :).
Perhaps another way to phrase this: in Rust, you spend more time telling the compiler how your code is expected to work (making the borrow checker happy, adding sync traits on objects you "know" are thread safe because of how you use them or assurances the underlying hardware provides, etc etc etc). In return, the compiler does a lot of work to make sure the code will actually work how you think it's going to work.
A good example is a simple producer-consumer problem. Make a ring buffer. Push the current sys clk tick count register to the ring buffer every time there's a rising edge interrupt on a GPIO (e.x. hook up a button or something to it). Poll the ring buffer in another thread and as soon as there is a timestamp in the buffer, pop it and log it to a UART.
Compare writing this in C vs Rust. In the text book implementation of a producer-consumer ring buffer, you don't need locks.
In C, this is a problem I'd expect a senior-level candidate to be able to knock out in a 60 minute interview.
(aside: I'd never actually ask a question like this in an interview, because it heavily favors people who just happen to be familiar with the algorithm, which doesn't give me enough signal about the candidate. This is borderline like asking someone to implement a sort algorithm in an interview - it just tells you they know how to google or memorize things. /aside).
(aside 2: If I did ask this, I'd be more interested how they design the thing - I'd be looking for questions like "how many events per second? How should I size the ring buffer? Wait, you want me to hook up an IRQ to a button? Is there a risk of interrupt storm from bounce or someone being malicious? Do you want me to add a cooldown timer between events? If we overflow the ring buffer, what should the behavior be? How fast can the UART go on this system - can it even keep up with the input?" - I'd be far more interested in that conversation than actually seeing them write code for this. /aside2).
In Rust, it's a bit more tricky. You'll need to give the compiler hints (in the form of Sync traits that are no-ops) to tell it you know what you're doing is thread safe. It's not rocket science, but the syntax is kind of weird and it will take some putzing around or aid from your favorite AI to get it right.
All of Rust ends up like this - you must be more verbose telling the compiler your intent. In exchange, it verifies the code matches your intent. So the up front cost is higher.
I suspect a lot of people will just pull a ring buffer off crates.io instead of figuring out the right incantations to make the compiler happy.
¯\_(ツ)_/¯ I'd like to lean into that "YMMV" in my post, I'm coming from low level C that interacts with hardware, can't really speak to higher-level C++.
Some things in Rust just don't translate to the way you'd do them in C - e.x. using different types to say if a GPIO pin is input or output adds a ton of boiler plate, but lets the compiler assure you don't mistakenly try and use a pin configured as an input for output.
In general, the whole zero sized types paradigm in Rust leads to way more lines of code to accomplish the same thing (it all ends up compiled out in the end though).
For embedded, I'll stand by what I said: it takes longer to write idiomatic Rust but you are more likely to get functionally correct code in the end.
ZSTs are a choice, if you're not benefiting from them in development time or correctness, why are you making that choice?
Rust isn't just C++ with static analysis. The safety is only about 1/3 of what makes me enjoy working with it. Even if I just consider the effort taken before I present something to a compiler, the trait system means I don't spend anywhere near as much time writing copy/copy assignment/move/move assignment constructors.
Some of that 2/3 will be me making choices like ZSTs, but I believe that's a result of it being necessary to grok the whole language and style and the idioms. If I just try to write C in Rust, it will be bad Rust and I'll have a bad time - but the same goes for just writing C++ as C with classes. I deliberately didn't say 'C/C++' in my ancestor post, because I think it's terminology that undermines claims of authority.
I do happen to have embedded experience, but avoid drawing conclusions from it because it is so different to more 'standard' systems development. If you can forgive a tangent: I avoid the whole domain now, because it feels like a ghetto. Standards are poor, tools are poor, pay is seriously poor. If you want to write embedded software, you probably have to work for a hardware company with hardware company's engineering culture and revenue model. That's a pretty crap place to write software in the west. I can't comment on comparing what like to do embedded development in Rust. Frankly, I hope I never can.
Good on you for recognizing what you enjoy and going for it.
I like hardware, always have :).
FWIW, I’ve done embedded at multiple mag 7 companies, and if levels.fyi is to be believed I’m getting paid the same as my peers doing other types of software. There are certainly places that don’t do this though (I hear the defense industry is notorious for this).
The point about the tooling being ghetto is spot on though, that’s part of why Rust is so exciting for us. A package manager?! What is this magic!!!
My experience in low-level stuff has been a mirror of the Linux kernel trying to take up Rust. Some people love it and fight hard for it, some hate it and fight hard against it, but most just see it as another tool in the toolbox and don't have strong feelings.
> Rust and JS are both famous for having minimal standard libraries
I'm all in favor of embiggening the Rust stdlib, but Rust and JS aren't remotely in the same ballpark when it comes to stdlib size. Rust's stdlib is decidedly not minimal; it's narrow, but very deep for what it provides.
C standard library is also very small. The issue is not the standard library. The issue is adding libraries for snippets of code, and in the name of convenience, let those libraries run code on the dev machine.
The issue is that our machines run 1970s OSes with a very basic security model, and are themselves so complex that they’re likely loaded with local privilege escalation attack vectors.
Doing dev in a VM can help, but isn’t totally foolproof.
It’s a good security model because everyone has the decency to follow a pull model. Like “hey, I have this thing, you can get it if you’re interested”. You decide the amount of trust you give to someone.
But NPM is more like “you’ve added me to your contact list, then it’s totally fine for me to enter your bedroom at night and wear your lingerie because we’re already BFF”. It’s “I’m doing whatever I want on your computer because I know best and you’re dumb” mentality that is very prevalent.
It’s like how zed (the editor) wants to install node.js and whatever just because they want to enable LSP. The sensible approach would have been to have a default config that relies on $PATH to find the language server.
> “you’ve added me to your contact list, then it’s totally fine for me to enter your bedroom at night and wear your lingerie because we’re already BFF”
I don't know if everyone will appreciate this, but I am in stitches right now... lol
Having worked on four different enterprise grade C# codebases, they most certainly have plenty of 3rd party dependencies. It would absolutely be the exception to not have 3rd party dependencies.
Yes, but the 3rd party dependencies tend to be conveniences rather than foundational. Easier mapping, easier mocking, easier test assertions, so a more security minded company can very easily just disallow their use without major impact. If it's something foundational to your project then what you're doing is probably somewhat niche. Most of the time there's some dependency from Microsoft that's rarely worse enough to justify using the 3rd party one.
I dunno, there's definitely stuff that's convenience related, sure, and then there's stuff that's less so. Things like MediatR, Dapper, Serilog, Refit are all pretty common in .NET projects. There's usually always some library for generating PDFs for reporting purposes etc. The ecosystem of NuGet packages is pretty large. It's definitely not all just Microsoft dependencies, nor is that usually how developers in the ecosystem think in my experience.
OP might be remembering how things were 10 years ago or so, before .NET Core became the .NET. Although there are still many companies with legacy codebases running on .NET 4.
That aside, dependency graphs in .NET land still tend to be much smaller.
This definitely not why enterprise "chooses" C# and neither of these were design decisions like implied. MS would have loved to have the explosive, viral ecosystem of Node earlier in .NET's life. Regardless a lot of companies using C# still use node-based solutions on the web so a insular development environment for one tier doesn't protect them.
I am not so sure about that. .net core is the moment they opened up, making it cross platform, going against the grain of owning it as a platform.
If they see a gap in .net, which is filled in by a third party, they would have no problem qualms about implementing their own solution in .net that meets their quality requirements. And to be fair, .net delivers on that.
This might anger some, but the philosophy is that it should be a batteries included one-stop shop, maybe driven by the culture of quite some ms shops that wouldn't eat anything unless ms feeds it them.
This has a consequence that the third-party ecosystem is a lot smaller, but I doubt MS regrets that.
If you compare that to F#, things are quite different wrt filling in the gaps, as MS does not focus on F#. A lot of good stuff for F# comes from the community.
They actually had a pretty active community on CodePlex - I used and contributed to many projects there... they killed that in ... checks the web... 2017, replaced with GitHub, and it just isn't the same...
And yet of course the world and their spouse import requests to fetch a URL and view the body of the response.
It would be lovely if Python shipped with even more things built in. I’d like cryptography, tabulate/rich, and some more featureful datetime bells and whistles a la arrow. And of course the reason why requests is so popular is that it does actually have a few more things and ergonomic improvements over the builtin HTTP machinery.
Something like a Debian Project model would have been cool: third party projects get adopted into the main software product by a sworn-in project member who who acts as quality control / a release manager. Each piece of software stays up to date but also doesn’t just get its main branch upstreamed directly onto everyone’s laps without a second pair of eyes going over what changed. The downside is it slows everything down, but that’s a side-effect of, or rather a synonym for stability, which is the problem we have with npm. (This looks sort of like what HelixGuard do, in the original article, though I’ve not heard of them before today.)
Requests is a great example of my point, actually. Creating a brand-new Python venv and running `uv add requests` tells me that a total of 5 packages were added. By contrast, creating a new Rust project and running `cargo add reqwest` (which is morally equivalent to Python's `requests`) results in adding 160 packages, literally 30x as many.
I don't think languages should try to include _everything_ in their stdlib, and indeed trying to do so tends to result in a lot of legacy cruft clogging up the stdlib. But I think there's a sweet spot between having a _very narrow_ stdlib and having to depend on 160 different 3rd-party packages just to make a HTTP request, and having a stdlib with 10 different ways of doing everything because it took a bunch of tries to get it right. (cf. PHP and hacks like `mysql_real_escape_string`, for example.)
Maybe Python also has a historical advantage here. Since the Internet was still pretty nascent when Python got its start, it wasn't the default solution any time you needed a bit of code to solve a well-known problem (I imagine, at least; I was barely alive at that point). So Python could afford to wait and see what would actually make good additions to the stdlib before implementing them.
Compare to Rust which _immediately_ had to run gauntles like "what to do about async", with thousands of people clamoring for a solution _right now_ because they wanted to do async Rust. I can definitely sympathize with Rust's leadership wanted to do the absolute minimum required for async support while they waited for the paradigm to stabilize. And even so, they still get a lot of flak for the design being rushed, e.g. with `Pin`.
So it's obviously a difficult balance to strike, and maybe the solution isn't as simple as "do more in the stdlib". But I'd be curious to see it tried, at least.
IMHO, the ideal for package management in a programming language ecosystem might recognise multiple levels of “standardisation”.
At the top, you have the true standard library for the language. This has very strong stability guarantees. Its purpose is twofold: to provide universal implementations of essentials and to define standard/baseline interfaces for common needs like abstract data types, relational databases, networking and filesystems to encourage compatibility and portability.
Next, you have a tier of recognised but not yet fully standardised libraries. These might be contributed by third parties, but they have requirements for identifying maintainers, appropriate licensing and mandatory peer review of all contributions. They have a clear versioning policy and can make breaking changes in new major releases, but they also provide some stability guarantees along the lines of semver and older releases are normally available indefinitely. The purpose of this tier is to provide a wider range of functionality and/or alternative implementations, but in a relatively stable way and implementing standard interfaces where applicable to improve portability.
Finally, you have the free-for-all, anyone-can-contribute tier. This should still have a sane security model where people can’t just upload malware scripts that run automatically just because someone installed a package. However, it comes with few guarantees about stability or compatibility, except that releases of published packages will be available indefinitely unless there’s a very good reason to pull them where you obviously wouldn’t want to use one anyway. A package you like might be written by a single contributor who no longer maintains it, but if someone does write something useful that simply doesn’t need any further maintenance once it’s finished and does its job, there is still a place to share it.
I agree with your general idea. I'd add it also looks very similar to what typical GNU/Linux have in practice: blessed packages from the distros' repositories, and third-party repos for those who want them.
Debian also has something 'in the middle' with additional repositories that aren't part of the main distribution and/or contain proprietary software.
Or maybe just get comfortable with adding versions and deprecation. eg optparse to argparse (though tbf, I would have just preferred it was optparse2). Or maybe the problem is excessive stability commitments. I think I prefer languages that realize things can improve and are willing to say if you want to run 10 year old code, use a 10 year old compiler/runtime.
I think I prefer languages that realize things can improve and are willing to say if you want to run 10 year old code, use a 10 year old compiler/runtime.
IMHO, the trouble with that stance is that it leaves no path to incrementally update a long-lived system to benefit from any of those improvements.
Suppose we have an application that runs on 2025’s most popular platform and in ten years we’re porting it to whatever new platform is popular in 2035. Personally, I’d like to know that all the business logic and database queries and UI structure and whatever else we wrote that was working before will still be working on the new platform, to whatever extent that makes sense. I’d like to make only some reasonably necessary set of changes for things that are actually different between the two platforms.
If we can’t do that, our only other option is a big rewrite. That is how you get a Python 2 to Python 3 situation. And that, in turn, is how you get a lot of systems stuck on the older version for years, despite all the advantages any later versions might offer.
Sure it does? Or do I not understand? It pushes the work to the application, not the language.
And it works fine on rails. If you want to stay on an EOL rails, you're on your own, or you can buy ongoing security backports from at least 2 (that I know of, maybe more) vendors. LTS lives roughly 2 years and then you either upgrade or deal with eol yourself.
That's not an apple-to-apple comparison, since Rust is a low-level language, and also because `reqwest` builds on top of `tokio`, an async runtime, and `hyper`, which is also a HTTP server, not just a HTTP client. If you check `ureq`, a synchronous HTTP client, it only adds 43 packages. Still more, but much less.
And in Go I can build a production-ready HTTPS (not just HTTP) server with just the standard library and a few lines of code. (0 packages).
That Rust does not have standard implementations of commonly-used features (such as an async runtime) is problematic for supply chain security, since then everyone is pulling in dozens (or hundreds) of fragmented 3rd-party packages instead of working with a bulletproof standard library.
And this is exactly why Go is winning: because it's actually rather easy to write "pure Go" utilities (no dependencies outside the standard library), which statically compile to boot (avoiding shared libraries).
> (cf. PHP and hacks like `mysql_real_escape_string`, for example.)
PHP is a fantastic resource to learn how to do proper backward compatibility and package management. By doing the exact opposite of whatever PHP does, mostly.
It might solve the problem, in as much as the problem is that not only can it be done, but it’s profitable to do so. This is why there’s no Rust problem (yet).
Most rust programmers are mediocre at best and really need the memory safety training wheels that rust provides. Years of nodejs mindrot has somehow made pulling into random dependencies irregular release schedules to become the norm for these people. They'll just shrug it off come up with some "security initiative* and continue the madness
I only personally know one Rust programmer (works in scientific HPC) and he’s fantastic, but in general I do get the sense that most Rust devs migrated from JS and are just now figuring out “omg strong typing and compiled code native to the client hardware is really nice!” and think it’s a ground breaking revelation.
Saying this as someone who is cautiously optimistic about Rust for my own work.
Like another commenter said, I do think it's partially just because dependency management is so easy in Rust compared to e.g. C or C++, but I also suspect that it has to do with the size of the standard library. Rust and JS are both famous for having minimal standard libraries, and what do you know, they tend to have crazy-deep dependency graphs. On the other hand, Python is famous for being "batteries included", and if you look at Python project dependency graphs, they're much less crazy than JS or Rust. E.g. even a higher-level framework like FastAPI, that itself depends on lower-level frameworks, has only a dozen or so dependencies. A Python app that I maintain for work, which has over 20 top-level dependencies, only expands to ~100 once those 20 are fully resolved. I really think a lot of it comes down to the standard library backstopping the most common things that everybody needs.
So maybe it would improve the situation to just expand the standard library a bit? Maybe this would be hiding the problem more than solving it, since all that code would still have to be maintained and would still be vulnerable to getting pwned, but other languages manage somehow.