10 ms·
Where Rust really shines
- jeorgun 12y agoMaybe I'm missing something, but why is it crucial that you hold on to a reference to the contents of the vector, and not the vector itself? Because, to me, the obvious "C++ way" to do it would be something like struct FieldInfo { // const std::vector<ast::Attribute>& attrs; }
- pcwalton 12y agoWell, if the object that owns the vector is destroyed then that reference will go dangling. The compiler can't (soundly) check this.
- lastontheboat 12y agoThat operates under the assumption that no FieldInfo will be accessible after the original vector is deallocated, and similarly requires auditing to ensure this property holds (while making no guarantees about the future).
- hoodedmongoose 12y agoThis is really exciting, and clearly more powerful than what you get in C/C++. That being said: >In a language like C++ there’s only once choice in this situation; that is to clone the vector. What about a shared_ptr to the vector?
- imron 12y agoDoesn't help, because you don't necessarily know when the other shared pointer could push something to vector, potentially reallocating the internal array and thereby invalidating any references you had to the contents of the vector.
- hoodedmongoose 12y agoAh okay - I didn't realize rust was guaranteeing that no one else would _modify_ the vector while an instance of that struct was around. That's pretty cool.
- imron 12y agoIt's very cool. Rust guarantees that at any point in time there will only be one part of the code that can modify the contents of some data, and will fail to compile if it detects a situation where this is not the case.
- Manishearth 12y agoRust follows multiple-readers or single-writer even in single-threaded situations. You can hold multiple immutable references to an object, or a single mutable one. Initially this gets annoying, but as time passes one realizes how awesome this is. It prevents iterator invalidation (and vector-move-invalidation), for one (it's also necessary to get memory safety for our ADT enums since you can otherwise change the variant and invalidate a reference to its contents). In general you realize that in a large codebase, it's almost as bad as a multithreaded situation -- you don't know what objects are being modified by the methods being called and the code is too large to easily figure it out.
- coherentpony 12y agoOr scoped_ptr to transfer ownership. There's certainly more than one choice, though.
- imron 12y agoNot really, and I say this as a fan of C++. These things protect against one part of the problem - deletion of the vector, but not the other part - mutation of the vector, causing a new internal allocation, and leaving references to any elements of the previous vector invalid. It's possible to make sure this isn't happening in C++ if a) your code base is small enough and b) you are careful, but the point of the article is that with Rust, you can make the changes and be confident that the compiler will fail if you do anything dangerous. With C++, you can make the changes and through careful checking be reasonably confident everything works (perhaps only to find out later that you were wrong). It's very different from having the compiler verify that you are not doing something unsafe.
- Manishearth 12y agoI couldn't have moved the vector, it was being used elsewhere.
- adwn 12y agoA shared_ptr does not prevent iterator invalidation caused by reallocation, because the pointed-to vector is not locked, or in any other form stopped from being modified. In Rust, when you have an immutable reference to a memory location, you know for certain that it won't be modified by distant code as long as you hold the reference. This guarantee also allows more compiler optimizations, because v[5] has the same value after a function call as before.
- hoodedmongoose 12y agoThanks for the explanation, I missed that!
- Manishearth 12y agoRemember that this was an API, exposing the `&[Attributes]` to external consumers. I may not want to hold pointers to the insides, but a consumer of the API might. Also, iterator invalidation. Also, runtime overhead of shared_ptr. (I added a footnote to the post to explain the issues with shared pointers)
- shafiee01 12y agoThis is really useful; I have been recently working on a huge c++ project and I had the exact same problem you mentioned with large vectors. The way I handled this problem was to wrap vector in a class which provides locking mechanism for expanding operations so while the vector is growing nobody can access it. I am excited to try Rust soon.
- deleted 12y ago[deleted]
- berkut 12y ago> And this is despite the fact that I still don’t know where that particular vector is modified or destroyed — I didn’t explore that far because I didn’t need to! (or want to :P) I find that a bit worrying - while it might well be an example of what's technically possible in some cases of using Rust to prevent segfault-type crashes, it doesn't mean there aren't now logic errors in the code which would do something just as bad. I guess it depends on how well you know the code and the context in which it's being used.
- tatterdemalion 12y ago> it doesn't mean there aren't now logic errors in the code which would do something just as bad Yes, it does. He wants to have immutable access to some data, the Rust borrow checker's rules provide certainty that the data is not deleted or mutated while his code is accessing it. What could happen that is "just as bad"? Data being referenced cannot be changed.
- tatterdemalion 12y agoI think an important thing to make sense of this is that Rust doesn't guarantee that what Manishearth did will work! Just that if it doesn't work, the code will not compile. It could have been the case that this vector was mutated or deleted at some point during the lifetime of his new struct; in that case, his code would not have compiled.
- berkut 12y agoIn this case it does. I was more talking about the overall not caring theme. I'd hope that's only because they know the code so can make those sort of assumptions. Are you saying Rust can find and prevent at compile time logic errors as well?
- tatterdemalion 12y agoYou "don't care," I'm sure, about all kinds of things when you're programming - depending on what level you're at. Unless you're drawing up CPU schematics, there is something that you have abstracted away as no longer your concern. Rust allows you to not care about more things than C/C++ despite compiling to a comparable binary. In particular, Rust makes refactoring and extending a code base much more care-free than many other languages (including high level languages) because of the strong type and memory safety guarantees of its semantics. EDIT (re: your edit) - No one said anything about preventing arbitrary logic errors, just that what Manishearth did will not introduce logic errors because of Rust's guarantees. This could not be the data that Manishearth wants, but the compiler guarantees that Manishearth cannot change the data or delete the data and that the data is not changed or deleted while he is referencing it. That is what is so great.
- SamReidHughes 12y agoThe article says you have to clone the vector, as if it's not completely normal to have one object hold a reference to another, "unsafely," in C++, without the world coming to an end. Having one object contain a reference or pointer to another in C++ is completely normal and something you certainly would "dream" of doing -- it happens all the time, you can find it everywhere in the STL, so I don't know what the blog author is getting at. Looking at the diff, the type SubstructureFields already has the lifetime parameter on it, and making FieldInfo, which is only used as a field in the SubstructureFields type (which owns its FieldInfo objects), be subject to the same lifetime constraint, is something that would be perfectly ordinary C++ and it registers near the bottom of the use-after-free risk scale.
- pcwalton 12y ago> Looking at the diff, the type SubstructureFields already has the lifetime parameter on it, and making FieldInfo, which is only used as a field in the SubstructureFields type (which owns its FieldInfo objects), be subject to the same lifetime constraint, is something that would be perfectly ordinary C++ and it registers near the bottom of the use-after-free risk scale. Which is why it works. But isn't it nice when the compiler is aware of the "use-after-free risk scale" for your data structures?
- SamReidHughes 12y agoI prefer technical accuracy in technical blog posts. If you want somebody to preach at about the merits of compiler-checked safety, it's not me.
- Manishearth 12y agoIt was accurate. See my comment below -- just because `SubstructureFields` had a lifetime parameter already doesn't mean it was borrowing data of a similar lifetime. That data being borrowed could probably live longer than the attributes, but fortunately the attributes live long enough that I can equate the lifetimes. I was able to use the same lifetime parameter by luck -- there was a good chance it wouldn't have compiled and I would have had to introduce another lifetime. (Though since this was a fix the compiler suggested I didn't need to worry too much about that happening -- generally those just work) Just having a lifetime doesn't mean that it's safe to put any random borrowed data in. In C++ we could have a single pointer to a very long-lived struct ("SubstructureFields"), and wish to introduce another struct ("FieldInfo") which contains a pointer to something that is shorter lived. Note that in large codebases knowing which is "longer lived" is not easy, so from the programmer's perspective there are just two pointers. Assuming that "Okay, we don't have any segfaults now due to the first pointer, introducing the second FieldInfo pointer should be fine then" would be fallacious -- we might be accessing data during a period of time when the first, original pointer is alive, but the second is invalidated. Use after free.
- thrownaway2424 12y agoA C++ function returning pointer to vector, or a public field of that type, would be considered bad API for the reasons mentioned in this article. The solution is to not do that. I'm not sure that Rust's ability to allow shared ownership of a vector makes this API pattern any better.
- Rusky 12y agoWhat's wrong with that API pattern if the compiler guarantees that it will be used correctly? It's certainly simpler and allows for better code reuse.
- lobster_johnson 12y agoBut it's a bad pattern in other languages because they don't guarantee safe access. Rust does, thus making it a good pattern. It's like saying that driving your car off a cliff is always a bad idea even when you've acquired a car that can fly.
- thrownaway2424 12y agoIt's a bad pattern in any language because it exposes too much details of storage layout and violates encapsulation. You probably don't want to expose internal implementation details in your API unless such exposure is critical to performance, and if it is critical to performance it seems unlikely that you'd be writing it in anything other than C++ anyway.
- lobster_johnson 12y agoThis seems orthogonal to internal implementation hiding, especially if the "API" is not a public library API, say, but an internal one. Then again, contracts about who can modify what data are also part of any public API; historically, there have been plenty of (quite decent) APIs that have returned pointers to internal structures that have limited lifetimes, in languages like C that don't enforce lifetimes at all.
- Matthias247 12y ago
- vbit 12y agoGarbage collected languages handle this fine too.
- jwilm 12y agoThey handle it fine by tracking references at runtime. The advantage here is compile time verification that the reference will stay valid for the life of the struct - zero runtime overhead.
- Manishearth 12y agoIterator invalidation is still a problem. Plus, having a GC for this would be quite heavy. The codebase was a compiler -- performance is necessary since a compiler's going to be slow anyway.
- steveklabnik 12y agoBut only for memory, and not for other kinds of resources.
- picardo 12y agoFor an outsider, Rust is still barely usable. I tried building a simple demo using the beta release today, and regretted right away. Lots of "unstable feature" errors that I can't figure out how to get rid of. Many useful features are unstable. I don't think I'm going to touch it again for a few months.
- rdtsc 12y agoSorry you had a bad experience. Just curious, have you tried asking for help on /r/rust or irc? I find Rust community is pretty friendly and helpful.
- tatterdemalion 12y agoWhat was unstable that you wanted to use? (Not trying to deny your experience: reporting this helps the Rust team determine what to prioritize)
- picardo 12y agoCollections. I tried to set the flag to allow them. This used to work in the dev release I downloaded 2 months ago, but there is a new error that says unstable features are not allowed in the beta release and I cannot override an outer flag –– or something like that. At that point, I just gave up.
- tatterdemalion 12y agoMost of the collections API is stable, & there may be a fairly simple alternative for the particular unstable feature you wanted to use. On the other hand, you could also download the nightly branch of Rust to be able to use all of the unstable features. I think the nature of the 1.0 release and the train model is not very easy to communicate: the Rust team hopes to commit strongly to semver, so "stable Rust" is only that which they are comfortable committing to providing backwards compatibility for indefinitely. It's really pretty fine to be on the nightly track if you don't mind occasionally having to change a method call or something when an unstable API you use change. Not trying to dismiss what you've said - I just think that the strength with which you expressed it ("Rust is still barely usable") was not accurate & that if you ask in the IRC channel or users.rust-lang.org, there is likely a no-or-little-hassle workaround for whatever feature you wanted.
- hckr1292 12y agoI've been following Rust for a long time now, and I'm very excited about the possibilities. However, I don't have a CS degree, and my work as a full stack web developer has only put me in contact with garbage collected languages: NodeJS, PHP, and a little Python. I just spent an entire week working on a chatbot in Rust, and I found the steep learning curve to be very challenging. While Rust seems to be a big win for teams that are accustomed to C++/C development, it does seem to be very demanding for web devs who always work with a garbage collector. In particular, I was trying to use Iron to write an http server that could receive web hooks. Iron, quite sensibly, handles each incoming request to a server in a separate thread. However, as a beginner it's very challenging to figure out how to persist data between requests without writing to disk or persisting to a db. Mutating state across threads is hard. Currently, there's also no version of channels that are single producer, multiple consumer. I'm sure as the ecosystem develops, more examples will make this easier, but for now it's surprisingly difficult to figure out. Fortunatey, the #rust IRC channel supported me all the way. It's a very friendly and active community, but without their support I would have probably given up. I look forward to seeing more posts at this level of detail help those of us struggling to learn Rust understand how the experts use Rust.
- Manishearth 12y agoI'm considering writing a GC for Rust next. It won't be as easy to use as the other GCs, but you will be able to put the ownership of stuff in the hands of the GC if you wish. Yeah, a lot of people are excited about Rust for webdev, but there is a steep learning curve and it's quite different from the languages most are used to. For webdev I personally think Go will make it, it feels a lot like Python/Ruby when programming, even though it's compiled and probably faster.
- Daishiman 12y agoI don't really think that's a problem with Rust so much as inexperience with languages that have no garbage collection. If anything, the problems you had would probably be exacerbated by languages like C and C++, which in many ways are deceptively simple, yet there are tricks to be aware of in every turn. Doing memory management is hard because of the nature of the problem.
- 12y ago
- alkonaut 12y agoAs a novice I find that when the compiler starts telling me do to things it will go one of two ways: either what the compiler tells me to add will quickly propagate until it's happy OR it will propagate until lifetime arguments have spread through the entire codebase, turning everything into token salad. The hard part is having a mental model of the data so you can reason about lifetimes. The compiler may help you to get it formally correct, but what would be really cool would be if it would help you find the "inversions" or "isolations" that you will need in order to not end up with lifetime soup.
- Manishearth 12y agoYum, salad. In my experience this happens when you overspecify a lifetime or something. After I got this code working, I messed with the lifetimes a bit -- I added one extra `'a` tying the lifetimes of something else to existing bound references. That propagated like wildfire and finally became unusable (proving that those two references could not/would not have the same lifetime). In more complex situations, it's generally worth thinking which lifetime should be bound together for the first few functions and structs. After that point just trust the compiler. It's not too hard to get a mental model of lifetimes, really, and once you have it you can look at lifetime parameters and figure out what they mean and how they work with each other. You're anyway implicitly thinking of them as scopes. I usually just ignore lifetime parameters -- they're something you learn to gloss over whilst reading Rust code, and I only really read them (and try to understand them) when I have lifetime errors. Sometimes the lifetime error is due to something unfixable, in which case the compiler will often lead me through a wild goose chase saladifying the entire codebase, or the lifetime error is fixable, in which case the compiler's suggestions usually work. Sometimes they don't, but in those situations you can still fix it by trying to understand the why of it (like how I did in this post, though in the case of this post the entire "understand the why of it" was an afterthought) because things worked.
- alkonaut 12y agoI bet there are a few common "patterns" to rust ownership, and equally many pitfalls and rookie mistakes when one fails to pick one of these patterns. It becomes extra hard if you approach Rust from a Java/Python/C# perspective where the slightest bit of allocation/RAII and mostly even thinking of the stack feels completely alien. You have to learn that and Rust. I'd love to see a kind of rust ownership tutorial in which you are asked to address a simple CS problem where these patterns occur in Rust. For example, many seem to find it hard to write a factory type. The next problem could be a doubly linked list and so on.
- shin_lao 12y agoActually having a constant reference as a member works as expected in C++ because the compiler will force you to initialize it a construction. It is true it will not check at compilation that you access a const reference of a destroyed object, but to be honest with RAII this is not really a problem as the lifetime of your objects should be pretty clear, by construction. In debug mode your program will quickly assert if you access a destroyed STL container. To be honest I think in this case: - either you want to snapshot the value and therefore you should clone the container (almost no performance cost for small containers) - you want to access the current value but then it means you know about the lifetime.
- al2o3cr 12y ago"the lifetime of your objects should be pretty clear, by construction" Given the potentially-unbounded consequences of triggering UB in C++, "pretty clear" seems like not enough.
- ExpiredLink 12y agoPeople don't choose a language. They choose a platform (or stack or environment). If you want to evaluate a language look at the language's platform. Currently Rust is not part of any platform but may become the language of the 'Mozilla platform' in the future.
- grayrest 12y agoThere's no overhead in caling a C api nor in other code calling Rust code as if it were a C api. There's FFI setup and some caveats both ways but a lot of the larger Rust projects involve (e.g Servo, Piston) interfacing with pre-existing C libraries.
- Manishearth 12y agoI don't think Mozilla plans to create a Rust-based platform, but the community is already making libraries that are useful at various levels of a stack.
- madez 12y agoPeople choose standards, not platforms. Some use just platform-specific standards, others use cross-platform standards. Rust doesn't need a platform, it needs support for cross-platform standards. This platform advocacy comes from what business needs (ability to extort money by gatekeeping), not what's technically sensible nor useful for the people.