I disagree with all the points in the article with the exception of lifetimes which i do find cumbersome at times due to the way they have to be propagated which makes refactoring to references awkward.
I think the larger standard library of a language like Go is appealing but Rust is a community effort and doesn't have Google scale resources to maintain a large standard library.
I particularly abhor Go style uppercase for public visibility and much prefer Rust's explicit syntax. In addition, i find Rust's module system very enjoyable and prefer it to any other language i have used once i got familiar with it.
Just making borrow checker violations into warnings for debug builds would go a long way to making the language more tolerable to the overwhelming majority of programmers not already committed to it.
Wanting to ship without violations does not mean you are not interested in getting your algorithm working, first. Often enough, you will abandon your laboriously borrow-safed code without shipping it, because you figured out a better way. But! you had to borrow-safe the new way, too, before you could even try it out.
Rust has already penetrated a good fraction of places that will tolerate it as it is now. To find wider usage, the user experience will have to get much better. Without finding enormously wider usage, it will fizzle. It will have fizzled: you only know after it is too late.
Fizzling is absolutely the normal fate of any new language. It takes absolutely phenomenal adoption to generate a lasting ecosystem. Even phenomenal adoption is not always enough. We once thought Ruby had legs, and Ada.
> Just making borrow checker violations into warnings for debug builds
This is an interesting idea. I wonder what class of borrowck problems could be ignored like. Some of them can't be ignored, I think, because they would allow catastrophic things like use-after-free. But are there other violations that aren't as serious?
If they are catastrophic, you may have to fix them to make progress. But most are not, and you can make other progress. Leaks are not. Cargo should not be dictating your work schedule.
I think this is valid reasoning. Allowing to turn errors into warnings for dev builds would probably allow for fixing issues faster. Given the UX is good.
I use todo!() a ton when building as well. Sketching out data types and signatures before diving into implementation details.
> Just making borrow checker violations into warnings for debug builds would go a long way to making the language more tolerable to the overwhelming majority of programmers not already committed to it.
The problem is that the borrow checker isn't just a verifier of correctness. It actually impacts the semantics of a program.
For example, in javascript if you open a TCP connection and let your connection handle go out of scope, the connection will stay open. And as a result, your program will sort of just hang. In Rust, when you open a connection and your handle goes out of scope, the socket is closed automatically. How does the compiler know when your program should close the socket? It knows based on the borrow checker's analysis of your program.
If your program confuses the borrow checker, that means that the compiler can't figure out when it should close that TCP connection on your behalf. This isn't the sort of thing you can just turn off in debug mode, because programs should act the same in debug and release modes. A program which doesn't close the TCP connection is a different program from one which does.
There are ways around this - you can use types like Box, String, Rc, Cell, etc. And then just .clone() all over the place. But your code will run slowly. And thats still way more annoying to use compared to other languages. I don't know any other language which makes you write "Box" to put an object on the heap.
I hear the criticism. It is legitimately really hard to learn how to write rust code in a way that makes the borrow checker happy, especially at first. I'm still avoiding async rust after getting my feet burned by it a couple of years ago. As another commentator has mentioned, rust makes you experience all the pain up front. Its a lot of pain for new programmers to the language - probably too much.
I genuinely believe rust will never achieve the popularity of languages like Python, Go or Javascript. Its just so much harder to learn. I don't think there's anything wrong with that. Most programmers don't need to write systems software. I'd rather rust keeps focuses hard on what its good at: situations where performance and correctness are more important than development speed. We have plenty of good programming languages for quickly prototyping things out. There's nothing wrong with using them when they're the right tools.
But sometimes you have other bugs you know about that you want to fix first. Or you want to try out something else, first. As long as your builds report violations, you will know you aren't done. You might suspect one or other warning is why something mysteriously doesn't work, and try tackling that. Or you know already, and have bigger fish to fry.
Before you are done, and ship, you will certainly have fixed everything, because your release build had to complete without violations. You are strictly better off making your own work schedule choices.
I would like for Rust to be used more in places where C is today. But that can't happen if it does not get wider acceptance.
The point isn't that there is a bug, it's that you haven't told the compiler enough information about what to do. It's kind of like writing a generic function signature and asking it to fill in the blank when there are many ways that can be done all with different semantics.
Haskell has type holes which cause a runtime error when executed as they cannot be evaluated which is an interesting approach (it also has other uses, such as figuring out types).
Exactly. The borrow "checker" is misnamed. Its part of the compiler. You can't skip borrowck and still produce a working executable for the same reason you can't skip linking.
Hm. Or can you? It might be possible to make a special "leaky" rust compiler mode where:
- Your program is only allowed to be single-threaded
- Nothing ever gets dropped. (All allocated memory leaks). And all variables get put on the heap.
- Mutable references can alias
That might be enough to make rust's lifetimes irrelevant. But I'm not convinced the juice is worth the squeeze.
If you don't mind segfaults, you could make it useful by copying C's semantics instead - but you'd need to manually free memory to avoid memory leaks. Or if you also don't care about performance and rust's ffi, you could put a tracing GC in there. But if you want that, why not just use Zig or Go?
> The program that is coded all in one go before ever compiling, and then never altered after, is rare as unicorns.
What? I never said that. Its bit rich to say I've missed the point, then immediately misinterpret me.
> Any working program being altered will have almost all its borrows neatly lined up.
Sure. My question is: When the lifetimes don't make sense, what should the compiler do if its expected to produce running code all the same? Should it leak memory? Should it produce code with undefined behaviour - like use-after-free errors and race conditions?
You seem to want code thats been explicitly marked as invalid by the borrow checker to still run. The rust compiler isn't designed to do that, and I'm trying to think through how the compiler could make invalid code run at all. What should even happen if you try to use a variable after handing over ownership?
Absolutely it should leak memory, if that is what it takes to get a program hobbling along. This is a debug build, we already know it doesn't work right, yet. If we didn't know before, the warning told us.
No program is going to ship with whatever bug it allows in, so who cares? The overwhelming majority of your undefined behaviors have no effect on what the person is interested in, right at that moment. They will get around to your crisis on their own time.
You are just insisting that your crisis be vanquished before they are allowed to look into theirs.
You then have two different languages. Its not a trivial decision to simply "leak" memory as you are saying. What happens with files? Do you corrupt the files? Do you not close TCP connections?
What you are proposing is to Rust to have two languages inside the same compiler. I guarantee you people would simply put debug builds in production if they could "turnoff" the borrow checker like you are proposing.
I am not suggesting anything even vaguely like what you are saying.
The compiler should emit whatever the code it sees says to emit. If it neglects to arrange to close TCP connections, they won't be closed. So what? I am not interested in TCP connections right now, or I would patch up that bit, in response to the warning the compiler spit out. I have other things to worry about right then.
The release build will insist everything be taken care of, so there is no "other language". You are just permitted to get there by a route you choose, in place the route Cargo would impose now.
Having worked some in Rust, and a lot in Haskell in my experience the design changes because of the borrow checker/type system (fixing the issues will provoke re-design, mostly for the better). Meaning it's not something you just flip on after the fact. The types can help guide and drive the design. They aren't a straitjacket to be taken off once code whack a mole is done.
That said, I do think Rust could add something like Haskell's type holes where instead of just randomly leaking memory the code has undefined behavior and will simply crash the program if executed.
> > Wanting to ship without violations does not mean you are not interested in getting your algorithm working, first. Often enough, you will abandon your laboriously borrow-safed code without shipping it, because you figured out a better way. But! you had to borrow-safe the new way, too, before you could even try it out.
This is a great idea! It made me realize why I still choose Python for prototyping. (I had been wondering...)
I absolutely love Rust's guarantees and prefer its memory management over GC. But nothing beats no memory management! Make everything Arc, make everything static---I don't care! My program will only live long enough to print "OK" and reach the next TODO, anyway.
> If your program confuses the borrow checker, that means that the compiler can't figure out when it should close that TCP connection on your behalf.
That's quite okay. "Process end" is as good a time as any, in this case.
> This isn't the sort of thing you can just turn off in debug mode, because programs should act the same in debug and release modes.
I don't know why programs with and without overflow and bounds checking are the "same" while programs that close unused sockets at different times are not, but it's okay---we don't have to call it "debug" mode. "Development" or "prototyping" mode both sound fine to me.
> There are ways around this - you can use types like Box, String, Rc, Cell, etc. And then just .clone() all over the place. But your code will run slowly. And thats still way more annoying to use compared to other languages. I don't know any other language which makes you write "Box" to put an object on the heap.
I think this is precisely the point. In other languages we can just create objects without worrying about where they should be stored. This is good because most of the objects -w-e- some people create don't get stored anywhere, anyway. They don't make it to the first release.
--
And yes, I realize I'm describing what is, from some angles, a completely different language. But I think one of the original discussion points is "why Rust-like languages keep popping up"? This is my take.
I didn't know where to squeeze this in, but the biggest annoyance in my (quite limited) time with Rust was: refactoring functions. I like breaking up functions into smaller chunks: `do_xy() { ... do_x(); ... do_y(); }` especially where a block of code could really benefit from being under its own name. Developing in Rust, I often found myself looking up internal types for the parameters/return type, and sometimes fighting the borrow checker. (I'm not sure type inference would be a good solution to the first, and I believe the second is mostly due to lexical lifetimes?)
To be pedantic, the borrow checker actually doesn't impact semantics.
The choice of which pointer type to use, e.g. a reference vs. Rc vs. Box, does impact semantics. And your broader point is well taken: since that choice affects semantics, it's really something you have to consider as you're designing your data structures; it doesn't really make sense to design the program first and tack on memory management at the end, like the parent is suggesting.
But that's not the borrow checker. For comparison, C++ has very similar behavior around smart pointers, but no borrow checker. Also, destructors are lexically scoped: a variable's destructor will be run when it goes out of scope, no earlier. The borrow checker used to be lexically scoped as well, but it's gotten smarter over time [1], and a borrow can now end in the middle of a block, if the borrow checker sees that the borrowed data is no longer used afterwards. Yet this has intentionally not been extended to include types with destructors, on the grounds that Rust code sometimes relies on destructor timing for correctness (in ways that the borrow checker can't see), so running destructors early could cause breakage, which would be hard for code authors to spot by just looking at the code. This same lack of predictability is considered acceptable for the borrow checker precisely because it doesn't affect runtime semantics.
While other languages with RC based GC mechanisms take care of managing the references automatically, even the C++ std ones do it, in Rust you have to manually increment the references with calls to clone().
It doesn't and I guess it will never get one, as it is apparently considered good practice that clone() calls are explicit, as in the developer is painfully made aware of the performance cost of reference counting.
So then we land in Rust code golf, where one tries to make the code as much as possible with bare bones borrow checker.
Not real clear on this... In C++, pimpl types almost always have a std::unique_ptr<Impl> in them, and they get moved, not copied. The compiler generates the move constructor implicitly, and move cannot fail. Maybe in Rust this just a struct with a ref in it? And what would be member functions borrow from it?
Ah, sorry. I misunderstood your point, so went into another path.
I don't see how this would work, Rust is already move by default, the problem is how to get std::shared_ptr() like convenience where copy member functions are called by the compiler, with the side effect of taking care of the counters implicitly.
In Rust, copying is explicit, thus manually typing the copy()/clone() calls.
Sorry, don't see how this touches on my question at all. I don't use shared_ptr in C++, and don't know any reason to make Arc nicer in Rust. My question was specifically about a Rust analog of the common idiom
> Just making borrow checker violations into warnings for debug builds would go a long way to making the language more tolerable to the overwhelming majority of programmers not already committed to it.
I hope Rust never does this, as I think it would kill the best thing about the language. There's almost no point in using Rust if you're not interested in leveraging the borrow checker to write safer code. Just write C++ instead.
The last thing the Rust community needs is to be flooded by new users who don't bring with them a culture of writing safe code. If you give users the ability to just turn off the borrow checker, they'll leave it off and ship code in debug mode.
Many users find the borrow checker perplexing only because they can't understand why the code they write day in and day out in other languages gets rejected by Rust. Well the answer is likely because it's unsafe in some subtle way, and the borrow checker caught it before you even had an inkling there was an issue. Best to get used to writing safer, well-architected systems first, and then you'll have an easier time satisfying the borrow checker.
Either Rust gets this, or almost everybody who might otherwise have coded Rust, someday, will be obliged to "write C++ instead". Or something. Letting your favorite language fizzle because you want to force everybody else's implementation schedule to match your biases is a peculiar choice.
I don't mind, personally, if Rust fizzles, but it seems like you ought to care.
There is an answer to this. Just `.clone()`, `.to_owned()`, `Arc::new`, and `Mutex::new` like a madman.
Sure it’s visual garbage. Yes it impacts the semantics of your program. Yes it wastes memory. Who gives a crap?
Those four things break you free of the borrow checker while also letting you get a grip on the language.
The docs don’t advocate this approach. Presumably because they favour correctness and understanding over blind hacks. I get why the docs are the way they are but I don’t think it’s worth the blood sacrifice necessary to stay on Mr. Borrow Checker’s Wild Ride.
Note that the above advice is no longer a 100% hammer-fits-all solution because futures and async and pin have unleashed brand new eldritch horrors upon us.
In other words, do enormously more work, that will then have to be undone, just for lack of a build flag. Nice.
Rust absolutely can still fizzle, and certainly will be found to have done, in time, without major improvements in adoption rate. If you have better ideas for that, propose those. The more the better.
Spurious objections to ways to improve adoption favor other languages.
Eh I wouldn’t say enormously. The latter two are hyper powerful but hyper rare because how often is a newbie using threads? The former two are just a method call you add to the offending line when the borrow checker complains, and not a moment sooner.
It tells you what’s wrong, you fix it, you move on. It’s no different than forgetting a semicolon in Java.
Nothing needs to be undone after. Either those 4 tools get you a valid program or you made a logic mistake you would’ve made in any other language. Or in Rust sans borrow checker. I don’t see how that’s an issue.
I don’t see how my methods are spurious objections or how I’ve spuriously objected to anything. My methods are tested, tried, and proven to work on a sample of n=1.
All that’s left is government funding and we can start mass human trials next month.
It has already been demonstrated to you that you just can’t “add a build flag”. Your suggestion is unworkable (no matter how many italicized words you sprinkle in). Adding `.to_owned()` will at least work (see, now I’m doing it too)—however much you may dislike it—and doesn’t suffer from the problem of being a pipe dream.
You can try to take your “fizzle” comments to /r/rust. See how many people care about Rust being on its deathbed due to not adopting your misguided suggestion.
The borrow checker is not hard. The difficulty is vastly overstated by web developers being told "no" to their shitty code for the first time in their life.
This is the "my leet lang is just for me, not for thee" argument. It is a sure route to there being no shops offering work coding in it. If that is your goal, you know how to get there. But you already are there.
You’re completely correct, but I think GP’s point is that we need said web developers to chase fancy new framework Flask-Native-Redux-Rust Edition™ to keep momentum going for the language as whole. Which personally I think has some merit.
Personally I think the understated way in which Rust now maintains a death grip on the WASM tool chain and tooling is the answer. Let the web devs have their JS. We can have everything `<canvas>` and below.
In a strange way, the borrow checker is old fashioned. It seems like an experience described by older programmers, of punching programs into cards and waiting for time on the computer, when they’d finally know if their program worked.
My few experiences with Rust made me realise that I’d become used to dropping into a Python shell to sort of explore and actually experience what was happening. Even in toy programs in Rust, I had to change my approach and also ensure I wrote one small, working chunk of code at a time. (Rather than roughly sketching out bigger sections, for example.)
I quite liked it, but changing habits isn’t easy and now I’m wondering if maybe it will limit the wider adoption of Rust.
I could actually see it work by adding a new keyword/macro to a variable that effectively gives the needed lifetime to the variable everywhere it is used. It couldn’t be compiled to a release build but indeed there are cases where one wants a working prototype first.
> I particularly abhor Go style uppercase for public visibility and much prefer Rust's explicit syntax.
I don't want to comment on Go's visibility here, but is it that much different than how in Rust the lack of a semi-colon means you're returning (or the presence of a semi-colon means it's an expression instead of a statement, which has tripped me up many more times than the inverse)?
I suppose in a way it is. With the semi-colon, it's present or not. Whereas uppercase for export is kind of mixing information. The casing of identifiers is syntax, not just stylistic - but only the first character. Otherwise, it's just for readability (e.g. valueOf vs valueof).
I've wondered whether the lifetime elision that happens sometimes on functions could be translated to structs too. So eg. by default all references in a struct are assumed to have the same lifetime, without any annotations. If you need multiple lifetimes or more control, then you add explicit parameters. That would simplify a whole lot of basic uses
> ... from a programmer's perspective, I would love to see a mix of compile-time lifetime analysis, Automatic Reference Counting (like in the lobster programming language), and manual memory management (marked as unsafe) for when extreme performance is needed.
Surprisingly, such a thing is actually possible. We can use borrow-checker-like static analysis layered on top of shared mutability (such as RC). It involves selectively making certain sub-graphs ("regions") immutable, thus getting the borrow checker's zero-cost memory safety, without the architectural restrictions the borrow checker normally imposes.
The core insight is that a borrow checker doesn't need every object to be completely affine, all it needs is some notion of what objects are reachable from what others.
This has recently been prototyped in the research languages from Forty2 ("capsules") [0] and from Milano, Turcotti, Myers [1].
Pony's "iso" [2] has shown that it can work remarkably well in production, and Vale [3] is making it work with single ownership.
In my (biased!) opinion, it's one of the most promising areas in language research. If we can get the speed and safety benefits of Rust while being more ergonomic and intuitive, I'd consider that a big step forward for the field. An exciting time to be in language design!
I totally agree. Literally every point is "what if Rust did this thing that bugs me the Golang way?" Granted, it's a sort list, but it seems like the patterns are precisely golang influenced.
There is nothing wrong with proposing changes from what, in your opinion, is a better implementation or workflow. Most of every language was implemented somewhere else first.
Sure, it's absolutely great to learn from other languages. I guess what rubs me the wrong way is every point is explicitly or implicitly the Golang way of doing things. There's no real representation from other languages. That combined with being a relatively short list makes the whole article underwhelming.
Article> It's the fruit of my experience working with C, Python, TypeScript, Go, and Rust and my frustrations with the latter.
I don't really see the influences from anything other than Go here.
Don't get me wrong, I love both Go and Rust, for very different reasons.
This person has no idea what they are talking about on the advanced point. The other points are a critique of Cargo not Rust, or just plain superficial nonsense.
Forcing you to change all usages seems like a win, making it harder to accumulate code smells and encouraging you to audit variable use — however briefly — when they change type.
the _ thing in python ime has been pointless, people will use those functions anyway if they need to. it just makes code ugly and harder to read. i rarely ever prefix a function with an _
I like it because it's an easy way to signal intent. Sure someone can still use it if they want, but it effectively communicates my desire that you _should not_ use this.
I'm open to other suggestions for how to effectively communicate the public interface without maintaining some separate documentation.
Visual C++ and clang tidy already offer something into that direction, based on the core guidelines, it isn't fully there due to the language's complexity.
At first, I was a little put off by how "batteries not included" the standard library was in rust, but I ended up preferring it. The quality of the rust community has generally made library selection pretty easy. There's usually a tacit first choice and often one or more optimized alternative for a particular situation or preference. When there is community disagreement and library churn, I think it ends up evolving better, more liked solutions; I truly believe this wouldn't happen as easily if it was enshrined in the std. Case and point, I've seen standard library implementations in C++ and python that just rot away.
The case sensitive visibility/privacy sound terrible to me. I dislike having to type pub as much as I do, but that seems worse for readability and clarity on top of throwing a wrench into the established rustfmt naming system.
About "Memory management", they already made some progress with non-lexical lifetimes in the past which did indeed improve the end user experience. I think "Polonius" aims to reduce the friction further, but I haven't looked into the specific improvements it might bring in user experience.
I think it has plenty enough of batteries for building things that can be used to build things. Very good abstractions for the most part, with mostly good implementations (ie. cases like parking_lot mutex show where it fails, and the `async` stuff is bit sketch).
Don't understand and don't support initiatives that want to stuff it full of actual applications and stuff (like http server, async runtime) etc. Just use I don't know, something else at that point.
> When there is community disagreement and library churn, I think it ends up evolving better, more liked solutions;
That's an optimistic way of thinking about. I feel hesitant to pull in libraries because you don't know when or why the community is going decide to change. Then your that fool using that old code, stuck with sudden tech debt.
It also makes stuff like stackoverflow weird to use where you get different answers depending on the year. I think the biggest problem is with libraries that try to make things more ergnomic. Learning about pin and how to use it is weird, everyone keeps recommending some higher level library.
Unfortunately a standard library doesn't really reduce churn. It just means to you chunks of the standard library being deprecated, which can be confusing. For example, what's the common way to download a file from the web in Python?
Instead, I think every language should have a list of batteries-suggested libraries, with a roughly 2 year turnover period. This way you get a community suggested group of the common functionality needed to be productive. It won't change so frequently that you're constantly looking up function names, but it also won't get sclerotic, and can move from old grungy libraries to new libraries when necessary. Of course, you can always just stick with an old "edition" of the community libs if you've got a legacy app that you don't want to migrate.
The author mentions "Serialization / Deserialization: JSON" as something the standard library needs. Would Rust have gotten `serde` in this case? I guess we'll never know, but serde is seriously impressive on many levels.
> I take a much different lesson than the author of this post about what lessons to take from Go's "large stdlib" experiment.
Personally, i think that this is simply a problem of not being able to throw the entire thing away, learn from the mistakes and make a better iteration.
I'm all for "batteries included" anything - be it a standard library for a programming language, an IDE with its own component libraries like Lazarus, a web development framework like Angular that's less fragmented than React or maybe even an operating system that has a lot of the software that you'll want to use preinstalled (or available on a DVD or a similar medium directly).
It's just that making sure that those batteries are good is impossible without iteration. Having to do backwards compatibility essentially makes that sort of iteration impossible. So what's the solution here?
Have a clearly defined end of life for the current version and a new major version in the future. Python 2 might have had a lot of warts, but Python 3 would clearly be better! One could say the same for Python 4, Python 5 and so on. The same goes for Go, Rust and any other high level language - just look at Java 8 and the newer iterations. You just need to learn to throw old things away instead of keeping them around in some half-alive state.
I say that as someone who's currently maintaining a Java 8 monolith that cannot be updated to anything more recent. If your software gets so large to the point where it cannot be migrated/rewritten in a manageable way, you've made a mistake. If you cannot feasibly migrate it over, then consider whether you even need it in the first place (e.g. build systems that you do less).
There are sub-optimal cases where that simply isn't possible: the Linux kernel comes to mind, as does a lot of other large legacy software that's slowly trudging along on the backs of tens of thousands of developer years. But somehow you don't see many new/existing systems using FORTRAN or COBOL and that's all for the better.
The lesson that I take is that all non-essential packages should instead be in an official, separately versioned, stdlib.
Go moved "image/draw" into "golang.org/x/image/draw". This was a good solution because it meant you could update the compiler without having to worry about also updating your image drawing stuff and that possibly breaking.
I wish they did that with http, tls, and all the other stuff that isn't really part of the language itself.
As is, I usually have to wait months to get compiler benefits (like being able to use generics, or smaller binary sizes, or fixes to bugs) because the compiler update also comes with breaking changes to the http package (like the http2 change) or tls or whatever.
My lessons is that there should be 3 tiers of libraries:
1. Compiler-versioned libraries. Things like io, syscalls, reflection, and things who's implementation depends heavily on the compiler internals. Breaking changes are never made without extreme circumstances.
2. Official, separately versioned, standard libraries. Things like golang.org/x/ or the idea behind the rust-lang-nursery. This is for stuff like http, logging, tls, crypto algorithms, image manipulation, high-level filesystem APIs, other protocols and abstractions. It is supported to use an old version of these libraries with a new compiler forever, and the compiler will never change libraries in 1 in such a way that they could break any old version of 2. You should update these whenever you can, but they may make breaking changes, and may be versioned as such.
3. The rest of the packages, stuff the community writes. These should prefer to use semver, and should prefer to use libraries from 1 and 2, but no promises are made.
I think having these 3 tiers is ideal, especially because it makes it easier to update the compiler (it will never break you), ensures perpetual support for older versions of official non-stdlib-but-still-core libraries (may make major releases that break, but the old one builds and works forever).
This is already pretty close to how C works actually, and it's great. Tier 1 is the actual language spec itself, linking libraries, etc. Tier 2 is libc. Tier 3 is the rest of the world. I really don't have to worry about how my pthread_create call in glibc will break when I update gcc because that separation is well maintained, and backwards compatibility is assured.
Python and go are both a clear step back, where I update the compiler to get some feature, and then my http server stops working even though I had no intention of updating it.
> The lesson that I take is that all non-essential packages should instead be in an official, separately versioned, stdlib.
Hmm, honestly, this is a really great take. A thin OS base, with the "batteries" being optional. Or at least being to use specific versions, bits and pieces from various points in time as needed.
Of course, this could turn into an Eldritch mess, but then again, it might also lead to working software over not being able to update the stuff you want to update because the rest would break otherwise.
Overall, there's probably a lot of consideration to be made about the details, but that seems workable. Stability might still sometimes be an issue (e.g. should the language internals change, a la Python 2 to 3), though i'm not sure whether that can even be addressed all that well, at least in the higher abstraction level languages (with separate runtimes like CLR, JVM etc.).
>At first, I was a little put off by how "batteries not included" the standard library was in rust, but I ended up preferring it. The quality of the rust community has generally made library selection pretty easy.
I still think it would be good to have a rust installer where you get a set of "almost standard" libraries pre-installed. So that in companies where you have to get everything approved, you could get this package approved.
There is no right answer. Too much core functionality defined by the community can be confusing and lead to mixed quality. As a newbie, how do I even know what the “rust SDK” even looks like and what parts are better than others.
A large official language SDK has its own downsides too though. For my money, the .NET base class library is the best “batteries included” platform out there. It has certainly had its mis-steps over the years though. How many projects that went all-in on the original ASP.NET ( now called Webforms ) are now totally stranded on a technology island that the broad .NET community is racing away from? How about the graveyard of UI frameworks for .NET? WCF? Using the “official” solutions did not help guarantee any longevity for these technologies ( though they are mostly still “supported” ).
It is hard really to say that one approach is superior. The Rust tool chain at least makes the distributes solution easier. For some stuff at least. The lack of a baked in library might make things like use in the Linux kernel harder in some ways.
I fail to see how decentralised package sources can possibly reduce the chances of a supply chain attack (from outsiders). Now instead of one repo needing to be secure and up to date, thousands must.
The short version is, it's better to have a centralised (though proxyable, and opt-outable) central store of hashes of module content, but then provide flexibility over distribution. The concern is that a central package manager adds complexity and opportunity for security vulnerabilities without adding much value.
If we're making wish-lists, I'd like a variant of Rust exactly as-is except with implicit boxing/GC instead of lifetimes and proper borrow-checking. The real version of Rust that has borrow-checking serves a more unique and important niche for many use-cases out there, but for most of my own personal use-cases I'd rather make this particular tradeoff while keeping everything else
I find I don't really care about that list, aside maybe trying to resolve the namesquatting problem in cargo.
My reduced list includes only:
* Strong typesafe wrapper types. We had a few RFCs, always dropped. This would actually change how I program, and would increase typesafety a lot. I don't even need fancy wrapping with the ability to select which traits to expose and which not, just wrap+automatic expose would change my life a lot
* Let me compile to shared libraries. I get it, all-static has its pros. But refusing to support shared libraries is asinine. It's fine if I have to always use the same rust compiler version! just give me the possibility!
I compile Rust to shared .dll and .so libs all the time. Is this what you're looking for?
--crate-type=dylib, #[crate_type = "dylib"] - A dynamic Rust library will be produced. This is different from the lib output type in that this forces dynamic library generation. The resulting dynamic library can be used as a dependency for other libraries and/or executables. This output type will create *.so files on Linux, *.dylib files on macOS, and *.dll files on Windows.
No, I guess I wrote the message too quickly and explained poorly, my bad.
I meant compiling a program and linking the crates as shared libraries. Kinda like a normal C/C++ program, and how the whole linux wolrd works.
...and I guess even the strong newtype wrapper is actually there with `struct Newtype(type);`, but I end up having way too many `.0` everywhere that I really dislike
I'll try not to post before my morning coffee next time :p
After reading this article I think the author just wanted to trigger everyone or that this is all some strange form of sarcasm/irony. No need to give it a second thought I guess...
86 comments
[ 3.6 ms ] story [ 282 ms ] threadI think the larger standard library of a language like Go is appealing but Rust is a community effort and doesn't have Google scale resources to maintain a large standard library.
I particularly abhor Go style uppercase for public visibility and much prefer Rust's explicit syntax. In addition, i find Rust's module system very enjoyable and prefer it to any other language i have used once i got familiar with it.
Wanting to ship without violations does not mean you are not interested in getting your algorithm working, first. Often enough, you will abandon your laboriously borrow-safed code without shipping it, because you figured out a better way. But! you had to borrow-safe the new way, too, before you could even try it out.
Rust has already penetrated a good fraction of places that will tolerate it as it is now. To find wider usage, the user experience will have to get much better. Without finding enormously wider usage, it will fizzle. It will have fizzled: you only know after it is too late.
Fizzling is absolutely the normal fate of any new language. It takes absolutely phenomenal adoption to generate a lasting ecosystem. Even phenomenal adoption is not always enough. We once thought Ruby had legs, and Ada.
This is an interesting idea. I wonder what class of borrowck problems could be ignored like. Some of them can't be ignored, I think, because they would allow catastrophic things like use-after-free. But are there other violations that aren't as serious?
I use todo!() a ton when building as well. Sketching out data types and signatures before diving into implementation details.
The problem is that the borrow checker isn't just a verifier of correctness. It actually impacts the semantics of a program.
For example, in javascript if you open a TCP connection and let your connection handle go out of scope, the connection will stay open. And as a result, your program will sort of just hang. In Rust, when you open a connection and your handle goes out of scope, the socket is closed automatically. How does the compiler know when your program should close the socket? It knows based on the borrow checker's analysis of your program.
If your program confuses the borrow checker, that means that the compiler can't figure out when it should close that TCP connection on your behalf. This isn't the sort of thing you can just turn off in debug mode, because programs should act the same in debug and release modes. A program which doesn't close the TCP connection is a different program from one which does.
There are ways around this - you can use types like Box, String, Rc, Cell, etc. And then just .clone() all over the place. But your code will run slowly. And thats still way more annoying to use compared to other languages. I don't know any other language which makes you write "Box" to put an object on the heap.
I hear the criticism. It is legitimately really hard to learn how to write rust code in a way that makes the borrow checker happy, especially at first. I'm still avoiding async rust after getting my feet burned by it a couple of years ago. As another commentator has mentioned, rust makes you experience all the pain up front. Its a lot of pain for new programmers to the language - probably too much.
I genuinely believe rust will never achieve the popularity of languages like Python, Go or Javascript. Its just so much harder to learn. I don't think there's anything wrong with that. Most programmers don't need to write systems software. I'd rather rust keeps focuses hard on what its good at: situations where performance and correctness are more important than development speed. We have plenty of good programming languages for quickly prototyping things out. There's nothing wrong with using them when they're the right tools.
But sometimes you have other bugs you know about that you want to fix first. Or you want to try out something else, first. As long as your builds report violations, you will know you aren't done. You might suspect one or other warning is why something mysteriously doesn't work, and try tackling that. Or you know already, and have bigger fish to fry.
Before you are done, and ship, you will certainly have fixed everything, because your release build had to complete without violations. You are strictly better off making your own work schedule choices.
I would like for Rust to be used more in places where C is today. But that can't happen if it does not get wider acceptance.
Haskell has type holes which cause a runtime error when executed as they cannot be evaluated which is an interesting approach (it also has other uses, such as figuring out types).
Hm. Or can you? It might be possible to make a special "leaky" rust compiler mode where:
- Your program is only allowed to be single-threaded
- Nothing ever gets dropped. (All allocated memory leaks). And all variables get put on the heap.
- Mutable references can alias
That might be enough to make rust's lifetimes irrelevant. But I'm not convinced the juice is worth the squeeze.
If you don't mind segfaults, you could make it useful by copying C's semantics instead - but you'd need to manually free memory to avoid memory leaks. Or if you also don't care about performance and rust's ffi, you could put a tracing GC in there. But if you want that, why not just use Zig or Go?
The program that is coded all in one go before ever compiling, and then never altered after, is rare as unicorns.
Any working program being altered will have almost all its borrows neatly lined up.
What? I never said that. Its bit rich to say I've missed the point, then immediately misinterpret me.
> Any working program being altered will have almost all its borrows neatly lined up.
Sure. My question is: When the lifetimes don't make sense, what should the compiler do if its expected to produce running code all the same? Should it leak memory? Should it produce code with undefined behaviour - like use-after-free errors and race conditions?
You seem to want code thats been explicitly marked as invalid by the borrow checker to still run. The rust compiler isn't designed to do that, and I'm trying to think through how the compiler could make invalid code run at all. What should even happen if you try to use a variable after handing over ownership?
No program is going to ship with whatever bug it allows in, so who cares? The overwhelming majority of your undefined behaviors have no effect on what the person is interested in, right at that moment. They will get around to your crisis on their own time.
You are just insisting that your crisis be vanquished before they are allowed to look into theirs.
What you are proposing is to Rust to have two languages inside the same compiler. I guarantee you people would simply put debug builds in production if they could "turnoff" the borrow checker like you are proposing.
I am not suggesting anything even vaguely like what you are saying.
The compiler should emit whatever the code it sees says to emit. If it neglects to arrange to close TCP connections, they won't be closed. So what? I am not interested in TCP connections right now, or I would patch up that bit, in response to the warning the compiler spit out. I have other things to worry about right then.
The release build will insist everything be taken care of, so there is no "other language". You are just permitted to get there by a route you choose, in place the route Cargo would impose now.
That said, I do think Rust could add something like Haskell's type holes where instead of just randomly leaking memory the code has undefined behavior and will simply crash the program if executed.
This is a great idea! It made me realize why I still choose Python for prototyping. (I had been wondering...)
I absolutely love Rust's guarantees and prefer its memory management over GC. But nothing beats no memory management! Make everything Arc, make everything static---I don't care! My program will only live long enough to print "OK" and reach the next TODO, anyway.
> If your program confuses the borrow checker, that means that the compiler can't figure out when it should close that TCP connection on your behalf.
That's quite okay. "Process end" is as good a time as any, in this case.
> This isn't the sort of thing you can just turn off in debug mode, because programs should act the same in debug and release modes.
I don't know why programs with and without overflow and bounds checking are the "same" while programs that close unused sockets at different times are not, but it's okay---we don't have to call it "debug" mode. "Development" or "prototyping" mode both sound fine to me.
> There are ways around this - you can use types like Box, String, Rc, Cell, etc. And then just .clone() all over the place. But your code will run slowly. And thats still way more annoying to use compared to other languages. I don't know any other language which makes you write "Box" to put an object on the heap.
I think this is precisely the point. In other languages we can just create objects without worrying about where they should be stored. This is good because most of the objects -w-e- some people create don't get stored anywhere, anyway. They don't make it to the first release.
--
And yes, I realize I'm describing what is, from some angles, a completely different language. But I think one of the original discussion points is "why Rust-like languages keep popping up"? This is my take.
I didn't know where to squeeze this in, but the biggest annoyance in my (quite limited) time with Rust was: refactoring functions. I like breaking up functions into smaller chunks: `do_xy() { ... do_x(); ... do_y(); }` especially where a block of code could really benefit from being under its own name. Developing in Rust, I often found myself looking up internal types for the parameters/return type, and sometimes fighting the borrow checker. (I'm not sure type inference would be a good solution to the first, and I believe the second is mostly due to lexical lifetimes?)
The choice of which pointer type to use, e.g. a reference vs. Rc vs. Box, does impact semantics. And your broader point is well taken: since that choice affects semantics, it's really something you have to consider as you're designing your data structures; it doesn't really make sense to design the program first and tack on memory management at the end, like the parent is suggesting.
But that's not the borrow checker. For comparison, C++ has very similar behavior around smart pointers, but no borrow checker. Also, destructors are lexically scoped: a variable's destructor will be run when it goes out of scope, no earlier. The borrow checker used to be lexically scoped as well, but it's gotten smarter over time [1], and a borrow can now end in the middle of a block, if the borrow checker sees that the borrowed data is no longer used afterwards. Yet this has intentionally not been extended to include types with destructors, on the grounds that Rust code sometimes relies on destructor timing for correctness (in ways that the borrow checker can't see), so running destructors early could cause breakage, which would be hard for code authors to spot by just looking at the code. This same lack of predictability is considered acceptable for the borrow checker precisely because it doesn't affect runtime semantics.
[1] https://stackoverflow.com/questions/50251487/what-are-non-le...
While other languages with RC based GC mechanisms take care of managing the references automatically, even the C++ std ones do it, in Rust you have to manually increment the references with calls to clone().
So then we land in Rust code golf, where one tries to make the code as much as possible with bare bones borrow checker.
I don't see how this would work, Rust is already move by default, the problem is how to get std::shared_ptr() like convenience where copy member functions are called by the compiler, with the side effect of taking care of the counters implicitly.
In Rust, copying is explicit, thus manually typing the copy()/clone() calls.
My initial remark was regarding the ergonomics of reference counting, naturally for values greater than 1.
The reason is exactly that, ergonomics and developer productivity.
A reason usually taken into account when picking programming languages for a project, if multiple options are available.
I hope Rust never does this, as I think it would kill the best thing about the language. There's almost no point in using Rust if you're not interested in leveraging the borrow checker to write safer code. Just write C++ instead.
The last thing the Rust community needs is to be flooded by new users who don't bring with them a culture of writing safe code. If you give users the ability to just turn off the borrow checker, they'll leave it off and ship code in debug mode.
Many users find the borrow checker perplexing only because they can't understand why the code they write day in and day out in other languages gets rejected by Rust. Well the answer is likely because it's unsafe in some subtle way, and the borrow checker caught it before you even had an inkling there was an issue. Best to get used to writing safer, well-architected systems first, and then you'll have an easier time satisfying the borrow checker.
It just needs to be good enough for people not bothering to change.
I don't mind, personally, if Rust fizzles, but it seems like you ought to care.
The docs don’t advocate this approach. Presumably because they favour correctness and understanding over blind hacks. I get why the docs are the way they are but I don’t think it’s worth the blood sacrifice necessary to stay on Mr. Borrow Checker’s Wild Ride.
Note that the above advice is no longer a 100% hammer-fits-all solution because futures and async and pin have unleashed brand new eldritch horrors upon us.
Technically those methods don't break free of the borrow checker; they satisfy the borrow checker by not borrowing.
Rust absolutely can still fizzle, and certainly will be found to have done, in time, without major improvements in adoption rate. If you have better ideas for that, propose those. The more the better.
Spurious objections to ways to improve adoption favor other languages.
It tells you what’s wrong, you fix it, you move on. It’s no different than forgetting a semicolon in Java.
Nothing needs to be undone after. Either those 4 tools get you a valid program or you made a logic mistake you would’ve made in any other language. Or in Rust sans borrow checker. I don’t see how that’s an issue.
I don’t see how my methods are spurious objections or how I’ve spuriously objected to anything. My methods are tested, tried, and proven to work on a sample of n=1.
All that’s left is government funding and we can start mass human trials next month.
You can try to take your “fizzle” comments to /r/rust. See how many people care about Rust being on its deathbed due to not adopting your misguided suggestion.
Exactly! And remember, it's not always getting a grip on the language. Sometimes it's getting a grip on the problem or one of the possible solutions.
Personally I think the understated way in which Rust now maintains a death grip on the WASM tool chain and tooling is the answer. Let the web devs have their JS. We can have everything `<canvas>` and below.
My few experiences with Rust made me realise that I’d become used to dropping into a Python shell to sort of explore and actually experience what was happening. Even in toy programs in Rust, I had to change my approach and also ensure I wrote one small, working chunk of code at a time. (Rather than roughly sketching out bigger sections, for example.)
I quite liked it, but changing habits isn’t easy and now I’m wondering if maybe it will limit the wider adoption of Rust.
I don't want to comment on Go's visibility here, but is it that much different than how in Rust the lack of a semi-colon means you're returning (or the presence of a semi-colon means it's an expression instead of a statement, which has tripped me up many more times than the inverse)?
Which has interesting consequences, e.g. for non-ASCII identifiers where there is no concept of lower- or uppercase: https://go.dev/doc/faq#unicode_identifiers
Whether or not this bothers you is a different question.
Edit: Found some prior discussion here https://users.rust-lang.org/t/why-no-lifetime-elision-for-st...
Surprisingly, such a thing is actually possible. We can use borrow-checker-like static analysis layered on top of shared mutability (such as RC). It involves selectively making certain sub-graphs ("regions") immutable, thus getting the borrow checker's zero-cost memory safety, without the architectural restrictions the borrow checker normally imposes.
The core insight is that a borrow checker doesn't need every object to be completely affine, all it needs is some notion of what objects are reachable from what others.
This has recently been prototyped in the research languages from Forty2 ("capsules") [0] and from Milano, Turcotti, Myers [1].
Pony's "iso" [2] has shown that it can work remarkably well in production, and Vale [3] is making it work with single ownership.
In my (biased!) opinion, it's one of the most promising areas in language research. If we can get the speed and safety benefits of Rust while being more ergonomic and intuitive, I'd consider that a big step forward for the field. An exciting time to be in language design!
[0] http://forty2.is/
[1] https://www.cs.cornell.edu/andru/papers/gallifrey-types/gall...
[2] https://tutorial.ponylang.io/reference-capabilities/referenc...
[3] https://verdagon.dev/blog/zero-cost-refs-regions
Article> It's the fruit of my experience working with C, Python, TypeScript, Go, and Rust and my frustrations with the latter.
I don't really see the influences from anything other than Go here.
Don't get me wrong, I love both Go and Rust, for very different reasons.
No it really, really shouldn’t.
Similar to Python's _foo. I don't like it because if you want to change the visibility you must change the name at all current use-sites.
I'm open to other suggestions for how to effectively communicate the public interface without maintaining some separate documentation.
The case sensitive visibility/privacy sound terrible to me. I dislike having to type pub as much as I do, but that seems worse for readability and clarity on top of throwing a wrench into the established rustfmt naming system.
About "Memory management", they already made some progress with non-lexical lifetimes in the past which did indeed improve the end user experience. I think "Polonius" aims to reduce the friction further, but I haven't looked into the specific improvements it might bring in user experience.
Don't understand and don't support initiatives that want to stuff it full of actual applications and stuff (like http server, async runtime) etc. Just use I don't know, something else at that point.
That's an optimistic way of thinking about. I feel hesitant to pull in libraries because you don't know when or why the community is going decide to change. Then your that fool using that old code, stuck with sudden tech debt.
It also makes stuff like stackoverflow weird to use where you get different answers depending on the year. I think the biggest problem is with libraries that try to make things more ergnomic. Learning about pin and how to use it is weird, everyone keeps recommending some higher level library.
Instead, I think every language should have a list of batteries-suggested libraries, with a roughly 2 year turnover period. This way you get a community suggested group of the common functionality needed to be productive. It won't change so frequently that you're constantly looking up function names, but it also won't get sclerotic, and can move from old grungy libraries to new libraries when necessary. Of course, you can always just stick with an old "edition" of the community libs if you've got a legacy app that you don't want to migrate.
It's wildly inefficient (mostly due to reflection), not type safe, and somewhat surprising. Guess the output https://go.dev/play/p/lSyiaN3IYOR
The author also mentioned "Images manipulation" as a thing that should exist in the standard library.
We actually know how that one went from go too: There's the stdlib image/draw (https://pkg.go.dev/image/draw). But generally, it's recommended to use the non-stdlib golang.org/x/image/draw package instead these days (https://pkg.go.dev/golang.org/x/image/draw)
For backwards compatibility reasons, the stdlib couldn't change, and it's actually far more confusing as a result.
I take a much different lesson than the author of this post about what lessons to take from Go's "large stdlib" experiment.
Personally, i think that this is simply a problem of not being able to throw the entire thing away, learn from the mistakes and make a better iteration.
I'm all for "batteries included" anything - be it a standard library for a programming language, an IDE with its own component libraries like Lazarus, a web development framework like Angular that's less fragmented than React or maybe even an operating system that has a lot of the software that you'll want to use preinstalled (or available on a DVD or a similar medium directly).
It's just that making sure that those batteries are good is impossible without iteration. Having to do backwards compatibility essentially makes that sort of iteration impossible. So what's the solution here?
Have a clearly defined end of life for the current version and a new major version in the future. Python 2 might have had a lot of warts, but Python 3 would clearly be better! One could say the same for Python 4, Python 5 and so on. The same goes for Go, Rust and any other high level language - just look at Java 8 and the newer iterations. You just need to learn to throw old things away instead of keeping them around in some half-alive state.
I say that as someone who's currently maintaining a Java 8 monolith that cannot be updated to anything more recent. If your software gets so large to the point where it cannot be migrated/rewritten in a manageable way, you've made a mistake. If you cannot feasibly migrate it over, then consider whether you even need it in the first place (e.g. build systems that you do less).
There are sub-optimal cases where that simply isn't possible: the Linux kernel comes to mind, as does a lot of other large legacy software that's slowly trudging along on the backs of tens of thousands of developer years. But somehow you don't see many new/existing systems using FORTRAN or COBOL and that's all for the better.
Go moved "image/draw" into "golang.org/x/image/draw". This was a good solution because it meant you could update the compiler without having to worry about also updating your image drawing stuff and that possibly breaking.
I wish they did that with http, tls, and all the other stuff that isn't really part of the language itself.
As is, I usually have to wait months to get compiler benefits (like being able to use generics, or smaller binary sizes, or fixes to bugs) because the compiler update also comes with breaking changes to the http package (like the http2 change) or tls or whatever.
My lessons is that there should be 3 tiers of libraries:
1. Compiler-versioned libraries. Things like io, syscalls, reflection, and things who's implementation depends heavily on the compiler internals. Breaking changes are never made without extreme circumstances.
2. Official, separately versioned, standard libraries. Things like golang.org/x/ or the idea behind the rust-lang-nursery. This is for stuff like http, logging, tls, crypto algorithms, image manipulation, high-level filesystem APIs, other protocols and abstractions. It is supported to use an old version of these libraries with a new compiler forever, and the compiler will never change libraries in 1 in such a way that they could break any old version of 2. You should update these whenever you can, but they may make breaking changes, and may be versioned as such.
3. The rest of the packages, stuff the community writes. These should prefer to use semver, and should prefer to use libraries from 1 and 2, but no promises are made.
I think having these 3 tiers is ideal, especially because it makes it easier to update the compiler (it will never break you), ensures perpetual support for older versions of official non-stdlib-but-still-core libraries (may make major releases that break, but the old one builds and works forever).
This is already pretty close to how C works actually, and it's great. Tier 1 is the actual language spec itself, linking libraries, etc. Tier 2 is libc. Tier 3 is the rest of the world. I really don't have to worry about how my pthread_create call in glibc will break when I update gcc because that separation is well maintained, and backwards compatibility is assured.
Python and go are both a clear step back, where I update the compiler to get some feature, and then my http server stops working even though I had no intention of updating it.
Hmm, honestly, this is a really great take. A thin OS base, with the "batteries" being optional. Or at least being to use specific versions, bits and pieces from various points in time as needed.
Of course, this could turn into an Eldritch mess, but then again, it might also lead to working software over not being able to update the stuff you want to update because the rest would break otherwise.
Overall, there's probably a lot of consideration to be made about the details, but that seems workable. Stability might still sometimes be an issue (e.g. should the language internals change, a la Python 2 to 3), though i'm not sure whether that can even be addressed all that well, at least in the higher abstraction level languages (with separate runtimes like CLR, JVM etc.).
I still think it would be good to have a rust installer where you get a set of "almost standard" libraries pre-installed. So that in companies where you have to get everything approved, you could get this package approved.
A large official language SDK has its own downsides too though. For my money, the .NET base class library is the best “batteries included” platform out there. It has certainly had its mis-steps over the years though. How many projects that went all-in on the original ASP.NET ( now called Webforms ) are now totally stranded on a technology island that the broad .NET community is racing away from? How about the graveyard of UI frameworks for .NET? WCF? Using the “official” solutions did not help guarantee any longevity for these technologies ( though they are mostly still “supported” ).
It is hard really to say that one approach is superior. The Rust tool chain at least makes the distributes solution easier. For some stuff at least. The lack of a baked in library might make things like use in the Linux kernel harder in some ways.
Edit: along with the authors own blog explaining their take: https://kerkour.com/supply-chain-attacks-and-backdoored-depe....
The short version is, it's better to have a centralised (though proxyable, and opt-outable) central store of hashes of module content, but then provide flexibility over distribution. The concern is that a central package manager adds complexity and opportunity for security vulnerabilities without adding much value.
My reduced list includes only:
* Strong typesafe wrapper types. We had a few RFCs, always dropped. This would actually change how I program, and would increase typesafety a lot. I don't even need fancy wrapping with the ability to select which traits to expose and which not, just wrap+automatic expose would change my life a lot
* Let me compile to shared libraries. I get it, all-static has its pros. But refusing to support shared libraries is asinine. It's fine if I have to always use the same rust compiler version! just give me the possibility!
I compile Rust to shared .dll and .so libs all the time. Is this what you're looking for?
https://doc.rust-lang.org/reference/linkage.htmlI meant compiling a program and linking the crates as shared libraries. Kinda like a normal C/C++ program, and how the whole linux wolrd works.
...and I guess even the strong newtype wrapper is actually there with `struct Newtype(type);`, but I end up having way too many `.0` everywhere that I really dislike
I'll try not to post before my morning coffee next time :p
Okay I see. You can kind of get this from the libloading crate.
https://docs.rs/libloading/latest/libloading/
It works but it’s not great.