10 ms·
The benefit of Zig seems to be that it allows you to keep thinking like a C programmer. That may be great, but to a certain extent it’s also just a question of
by simonask 1y ago
The benefit of Zig seems to be that it allows you to keep thinking like a C programmer. That may be great, but to a certain extent it’s also just a question of habit.
Seasoned Rust coders don’t spend time fighting the borrow checker - their code is already written in a way that just works. Once you’ve been using Rust for a while, you don’t have to “restructure” your code to please the borrow checker, because you’ve already thought about “oh, these two variables need to be mutated concurrently, so I’ll store them separately”.
The “object soup” is a particular approach that won’t work well in Rust, but it’s not a fundamentally easier approach than the alternatives, outside of familiarity.
- Galanwe 1y ago> Seasoned Rust coders don’t spend time fighting the borrow checker My experience is that what makes your statement true, is that _seasoned_ Rust developers just sprinkle `Arc` all over the place, thus effectively switching to automatic garbage collection. Because 1) statically checked memory management is too restrictive for most kinds of non trivial data structures, and 2) the hoops of lifetimes you have to go to to please the static checker whenever you start doing anything non trivial are just above human comprehension level.
- steveklabnik 1y agoThe only time I use Arc is wrapping contexts for web handlers. That doesn’t mean there aren’t other legitimate use cases, but “all the time” is not representative of the code I read or write, personally.
- amw-zero 1y agoHow often are you writing non-trivial data structures?
- swiftcoder 1y agoI don't think there are any Arcs in my codebase (apart from a couple of regrettable ones needed to interface with Javascript callbacks in WASM - this is more a WASM problem than a rust problem).
- ChadNauseam 1y agohaha, I was about to leave the exact same comment. how are you finding wasm? I’ve been feeling like rust+react is my new favorite tech stack
- swiftcoder 1y agoI love it, but I'm mainly using it for webgl/webgpu stuff, so relatively little interaction with the DOM - I feel like DOM interaction is still kind of painful through rust/wasm
- dev_l1x_be 1y agoI am not sure this is true. Maybe with shared async access it is. I rarely use Arc.
- qw3rty01 1y agoThis is exactly the opposite of what he’s saying, using Arc everywhere is hacking around the borrow checker, a seasoned rust developer will structure their code in a way that works with the borrow checker; Arc has a very specific use case and a seasoned rust developer will rarely use it
- jasonjmcghee 1y agoThis is awkward. I've written a fair amount of rust. I reach for Arc frequently. I see the memory layout implications now. Do you tend to use a lot of Arenas?
- dminik 1y agoI've not explored every program domain, but in general I see two kinds of program memory access patterns. The first is a fairly generic input -> transform -> output. This is your generic request handler for instance. You receive a payload, run some transform on that (and maybe a DB request) and then produce a response. In this model, Arc is very fitting for some shared (im)mutable state. Like DB connections, configuration and so on. The second pattern is something like: state + input -> transform -> new state. Eg. you're mutating your app state based on some input. This fits stuff like games, but also retained UIs, programming language interpreters and so on on. Using ARCs here muddles the ownership. The gamedev ecosystem has found a way to manage this by employing ECS, and while it can be overkill, the base DOD principles can still be very helpful. Treat your data as what it is; data. Use indices/keys instead of pointers to represent relations. Keep it simple. Arenas can definitely be a part of that solution.
- packetlost 1y agoArc<T> is all over the place if you're writing async code unfortunately. IMO Tokio using a work-stealing threaded scheduler by default and peppering literally everything with Send + Sync constraints was a huge misstep.
- vlovich123 1y agoArc + work stealing scheduler is common. But work stealing schedulers are common (eg libdispatch popularized it). I believe the only alternative is thread-per core but they’re not very common/popular. For what it’s worth zig would look very similar except their novel injectable I/O syntax isn’t compatible with work stealing. Even then, I’d agree that while Arc is used in lots of places in work stealing runtimes, I disagree that it’s used everywhere or that you can really do anything else if you want to leverage all your cores with minimum effort and not having to build your application specialized to deal with that.
- kibwen 1y ago> _seasoned_ Rust developers just sprinkle `Arc` all over the place No, this couldn't be further from the truth.
- 9rx 1y agoIf they aren't sprinkling `Arc` all over, what are they seasoning with instead?
- veber-alex 1y ago'a
- 9rx 1y agoCan't be. Lifetime annotations are present in unseasoned Rust. The question was about what seasoning are being added, if not `Arc`?
- oconnor663 1y agoIndexes: https://jacko.io/object_soup.html https://jacko.io/object_soup.html
- andrewl-hn 1y ago`Arc`s show up all over the place specifically in async code that targets Tokio runtime running in multithreaded mode. Mostly this is because `tokio::spawn` requires `Future`s to be `Send + 'static`, and this function is a building block of most libraries and frameworks built on top of Tokio. If you use Rust for web server backend code then yes, you see `Arc`s everywhere. Otherwise their use is pretty rare, even in large projects. Rust is somewhat unique in that regard, because most Rust code that is written is not really a web backend code.
- khuey 1y ago> `Arc`s show up all over the place specifically in async code that targets Tokio runtime running in multithreaded mode. Mostly this is because `tokio::spawn` requires `Future`s to be `Send + 'static`, and this function is a building block of most libraries and frameworks built on top of Tokio. To some extent this is unavoidable. Non-'static lifetimes correspond (roughly) to a location on the program stack. Since a Future that suspends can't reasonably stay on the stack it can't have a lifetime other than 'static. Once it has to be 'static, it can't borrow anything (that's not itself 'static), so you either have to Copy your data or Rc/Arc it. This, btw, is why even tokio's spawn_local has a 'static bound on the Future. It would be nice if it were ergonomic for library authors to push the decision about whether to use Rc<RefCell<T>> or Arc<Mutex<T>> (which are non-threadsafe and threadsafe variants of the same underlying concept) to the library consumer.
- bryal 1y agoNon-tokio runtimes manage just fine without all of those bounds for local, single-threaded execution though. https://docs.rs/smol/latest/smol/fn.block_on.html https://docs.rs/smol/latest/smol/fn.block_on.html
- khuey 1y agoBlocking on an asynchronous task and waiting for it to finish is fundamentally a different operation than spawning an asynchronous operation and returning to the caller to execute entirely different code. smol's spawn also requires the Future to be 'static (https://docs.rs/smol/latest/smol/fn.spawn.html https://docs.rs/smol/latest/smol/fn.spawn.html), while tokio's local block_on also does not require 'static or Send + Sync (https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html#method.block_on https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...).
- hu3 1y agoI did some quick search, not sure if this supports or denies your point: - 151 instances of "Arc<" in Servo: https://github.com/search?q=repo%3Aservo%2Fservo+Arc%3C&type=code https://github.com/search?q=repo%3Aservo%2Fservo+Arc%3C&type... - 5 instances of "Arc<" in AWS SDK for Rust https://github.com/search?q=repo%3Arusoto%2Frusoto%20Arc%3C&type=code https://github.com/search?q=repo%3Arusoto%2Frusoto%20Arc%3C&... - 0 instances for "Arc<" in LOC https://github.com/search?q=repo%3Acgag%2Floc%20Arc%3C&type=code https://github.com/search?q=repo%3Acgag%2Floc%20Arc%3C&type=...
- qzw 1y agoAppreciate the real numbers. Would be interesting to see what percentage of data structures contain Arc, but that's a lot more work.
- tptacek 1y agoWhy would you expect the AWS SDK to have complicated memory management?
- liuliu 1y ago- 454 instances of "Rc<" in Servo: https://github.com/search?q=repo%3Aservo%2Fservo+Rc%3C&type=code https://github.com/search?q=repo%3Aservo%2Fservo+Rc%3C&type=... - 6 instances of "Rc<" in AWS SDK for Rust: https://github.com/search?q=repo%3Arusoto%2Frusoto+Rc%3C&type=code https://github.com/search?q=repo%3Arusoto%2Frusoto+Rc%3C&typ... - 0 instance for "Rc<" in LOC: https://github.com/search?q=repo%3Acgag%2Floc+Rc%3C&type=code https://github.com/search?q=repo%3Acgag%2Floc+Rc%3C&type=cod... (Disclaimer: I don't know what these repos are except Servo).
- levkk 1y agoDefinitely not. Arc is for immutable (or sync, e.g. atomics, mutexes) data, while borrow checker protects against concurrent mutations. I think you meant Arc<Mutex<T>> everywhere, but that code smells immediately and seasoned Rust devs don't do that.
- Aurornis 1y ago> thus effectively switching to automatic garbage collection Arc isn't really garbage collection. It's like a reference counted smart pointer like C++ has shared_ptr. If you drop an Arc and it's the last reference to the underlying object, it gets dropped deterministically. Garbage collection generally refers to more complex systems that periodically identify and free unused objects in a less deterministic manner.
- jcelerier 1y ago> Arc isn't really garbage collection. It's like a reference counted smart pointer like C++ has shared_ptr. In c++ land this is very often called garbage collection too
- jandrewrogers 1y agoThis still raises the question of why Arc is purportedly used so heavily. I've written 100s of kLoC of modern systems C++ and never needed std::shared_ptr.
- pjmlp 1y agoFor the same reason Unreal uses one. Large scale teams always get pointer ownership wrong. Project Zero has enough examples.
- Arnavion 1y ago>Garbage collection generally refers to more complex systems that periodically identify and free unused objects in a less deterministic manner. No, this is a subset of garbage collection called tracing garbage collection. "Garbage collection" absolutely includes refcounting.
- simonask 1y agoThere’s just no good reason to conflate the two. Rust’s Arc and C++’s std::shared_ptr do not reclaim reference cycles, so you can call it “garbage collection” if you want, but the colloquial understanding is way more useful.
- kannanvijayan 1y agoNot sure how seasoned I am, but I reject any comparison to a cooking utensil! I do find myself running into lifetime and borrow-checker issues much less these days when writing larger programs in rust. And while your comment is a bit cheeky, I think it gets at something real. One of the implicit design mentalities that develops once you write rust for a while is a good understanding of where to apply the `UnsafeCell`-related types, which includes `Arc` but also `Rc` and `RefCell` and `Cell`. These all relate to inner mutability, and there are many situations where plopping in the right one of these effectively resolves some design requirement. The other idiomatic thing that happens is that you implicitly begin structuring your abstract data layouts in terms of thunks of raw structured data and connections between them. This usually involves an indirection - i.e. you index into an array of things instead of holding a pointer to the thing. Lastly, where lifetimes do get involved, you tend to have a prior idea of what thing they annotate. The example in the article is a good case study of that. The author is parsing a `.notes` file and building some index of it. The text of the `.notes` file is the obvious lifetime anchor here. You would write your indexing logic with one lifetime 'src: `fn build_index<'src>(src: &'src str)` Internally to the indexing code, references to 'src-annotated things can generally pass around freely as their lifetime converges after it. Externally to the indexing code you'd build a string of the notes text, and passing a reference to that to the `build_index` function. For simple CLI programs, you tend not to really need anything more than this. It gets more hairy if you're looking at constructing complex object graphs with complex intermediate state, partial construction of sub-states, etc. Keeping track of state that's valid at some level, while temporarily broken at another level, is where it gets really annoying with multiple nested lifetimes and careful annotation required. But it was definitely a bit of a hair-pulling journey to get to my state of quasi-peace with Rust's borrow checker.
- bryanlarsen 1y agoOr more likely, sprinkle .clone() liberally and Arc or an Arc wrapper (ArcSwap, tokio's watch channels, etc) strategically.
- ViewTrick1002 1y ago> My experience is that what makes your statement true, is that _seasoned_ Rust developers just sprinkle `Arc` all over the place, thus effectively switching to automatic garbage collection. How else would you safely share data in multi-threaded code? Which is the only reason to use Atomic reference counts.
- nicoburns 1y ago100% I came to Rust from a primarily JavaScript/TypeScript background, and most of the idioms and approaches I was used to using translated directly into Rust.
- christophilus 1y agoIncluding pulling in hundreds of dependencies, unfortunately.
- dayvster 1y ago> Seasoned Rust coders don’t spend time fighting the borrow checker No true scotsman would ever be confused by the borrow checker. i've seen plenty of rust projects open source and otherwise that utilise Arc heavily or use clone and/or copy all over the place.
- Ygg2 1y ago> > Seasoned Rust coders don’t spend time fighting the borrow checker > No true scotsman would ever be confused by the borrow checker. I'd take that No true scotsman over the "Real C programmers write code without CVE" for $5000. Also you are strawmanning the argument. GP said, "As a seasoned veteran of Rust you learn to think like the borrow checkers." vs "Real Rust programmers were born with knowledge of borrow checker".
- fkyoureadthedoc 1y agoI'm starting to think No True HNer goes without misidentifying a No True Scotsman fallacy. They are clearly just saying as you become more proficient with X, Y is less of a problem. Not that if the borrow checker is blocking you that you aren't a real Rust programmer. Let's say you're trying to get into running. You express that you can't breathe well during the exercise and it's a miserable experience. One of your friends tells you that as an experienced runner they don't encounter that in the same way anymore, and running is thus more enjoyable. Do you start screeching No True Scotsman!! at them? I think not.
- vrmiguel 1y agoWhy would using Arc mean that someone is fighting the borrow checker, or confused by it? Would you also say the same for a C++ project that uses shared_ptrs everywhere? The clone quip doesn't work super well when comparing to C++ since that language "clones" data implicitly all the time
- dev_l1x_be 1y agoI can't remember the last time I had any problem with the borrow checker. The junior solution is .clone(), better one is & (reference) and if you really need you can start to use <'a>. There is a mild annoyance with which function consumes what and the LLM era really helped with this. My beef is sometimes with the ways traits are implemented or how AWS implemented Errors for the their library that is just pure madness.
- seivan 1y agoHow did AWS mess up errors?
- dev_l1x_be 1y agoMaybe I am holding it wrong. Here is one piece of the problem: while let Some(page) = object_stream.next().await { match page { // ListObjectsV2Output Ok(p) => { if let Some(contents) = p.contents { all_objects.extend(contents); } } // SdkError<ListObjectsV2Error, Response> Err(err) => { let raw_response = &err.raw_response(); let service_error = &err.as_service_error(); error!("ListObjectsV2Error: {:?} {:?}", &service_error, &raw_response); return Err(S3Error::Error(format!("ListObjectsV2Error: {:?}", err))); } } }
- estebank 1y agoOut of curiosity, why are you borrowing that many times? The following should work: while let Some(page) = object_stream.next().await { match page { // ListObjectsV2Output Ok(p) => { if let Some(contents) = p.contents { all_objects.extend(contents); } } // SdkError<ListObjectsV2Error, Response> Err(err) => { let raw_response = err.raw_response(); let service_error = err.as_service_error(); error!("ListObjectsV2Error: {:?} {:?}", service_error, raw_response); return Err(S3Error::Error(format!("ListObjectsV2Error: {:?}", err))); } } } I would have written it this way while let Some(page) = object_stream.next().await { let p: ListObjectsV2Output = page.map_err(|err| { // SdkError<ListObjectsV2Error, Response> let raw_response = err.raw_response(); let service_error = err.as_service_error(); error!("ListObjectsV2Error: {service_error:?} {raw_response:?}"); S3Error::Error(format!("ListObjectsV2Error: {err:?}")) })?; if let Some(contents) = p.contents { all_objects.extend(contents); } } although if your crate defines `S3Error`, then I would prefer to write while let Some(page) = object_stream.next().await { if let Some(contents) = page?.contents { all_objects.extend(contents); } } by implementing `From`: impl From<SdkError<ListObjectsV2Error, Response>> for S3Error { fn from(err: SdkError<ListObjectsV2Error, Response>) -> S3Error { let raw_response = err.raw_response(); let service_error = err.as_service_error(); error!("ListObjectsV2Error: {service_error:?} {raw_response:?}"); S3Error::Error(format!("ListObjectsV2Error: {err:?}")) } }
- melodyogonna 1y agoI believe the benefit of Zig is that it allows you the familiarity of writing code like in C, but has other elements in the language and tooling to make things safer. For example, Zig has optionals, which can eliminate nil deference. Another example is how you can pass some debug or custom allocators during testing that have all sorts of runtime checks to detect bad memory access and resource leaks. I have some issues with Zig's design, especially around the lack of explicit interface/trait, but I agree with the post that it is a more practical language, just because of how much simpler its adoption is.
- amelius 1y ago> Seasoned Rust coders don’t spend time fighting the borrow checker Yes, they know when to give up.
- Ygg2 1y agoNah. Just learn to think like it.
- seivan 1y ago[dead]
- tialaramex 1y ago> Seasoned Rust coders don’t spend time fighting the borrow checker I like the fact that "fighting the borrow checker" is an idea from the period when the borrowck only understood purely lexical lifetimes. So you have to fight to explain why the thing you wrote, which is obviously correct, is in fact correct. That's already ancient history by the time I learned Rust in 2021. But, this idea that Rust will mean "fighting the borrow checker" took off anyway even though the actual thing it's about was solved. Now for many people it really is a significant adjustment to learn Rust if your background is exclusively say, Python, or C, or Javascript. For me it came very naturally and most people will not have that experience. But even if you're a C programmer who has never had most of this [gestures expansively] before you likely are not often "fighting the borrow checker". That diagnostic saying you can't make a pointer via a spurious mutable reference? Not the borrow checker. The warning about failing to use the result of a function? Not the borrow checker. Now, "In Rust I had to read all the diagnostics to make my software compile" does sound less heroic than "battling with the borrow checker" but if that's really the situation maybe we need to come up with a braver way to express this.
- Austizzle 1y agoI think the phrase _emotionally_ resonates with people who write code that would work in other languages, but the compiler rejects. When I was learning rust (coming from python/java) it certainly felt like a battle because I "knew" the code was logically sound (at least in other languages) but it felt like I had to do all sorts of magic tricks to get it to compile. Since then I've adapted and understand better _why_ the compiler has those rules, but in the beginning it definitely felt like a fight and that the code _should_ work.
- zorked 1y agoAh, thanks for the historical take. I learned Rust recently. I like it. I never fought the borrow checker. I was sometimes happily protected by the borrow checker. I never understood what people were talking about.
- tialaramex 1y agoRight, in early Rust, years ago, it's not even legal to do this: fn main() { let mut x = 5; let y = &x; let z = &mut x; } The original borrowck goes oh no, y is a reference to x and then z is a mutable reference to x, those can't both exist at the same time, but the scope of y hasn't ended so I give up here's an error message. You needed to adjust your software so that it can see why what you wrote is fine. That's "fighting the borrow checker". But today's borrowck just goes duh, the reference y goes away right before the variable z is created and everything is cool. These are called "Non-lexical lifetimes" because the lifetime is no longer strictly tied to a lexical scope - the curly braces in the program - but can have any necessary extent to make things correct. Further improving the ability of the borrowck to see that what you're doing is fine is an ongoing piece of work for Rust and always will be†, but NLL was the lowest hanging fruit, most of the software I write would need tweaks to account for a strict lexical lifetime and it'd be very annoying when I know I am correct. † Rice's theorem tells us we can either have a compiler where sometimes illegal borrows are allowed or a compiler where sometimes borrows that should be legal are forbidden (or both, which seems useless), but we cannot have one which is always right, so, Rust chooses the safe option and that means we're always going to be working to make it just a bit better.
- tleyden5iwx 1y agoI don't think like a C programmer, my problem is that I think like a Java/Python/Go programmer, and I'm spoiled by getting used to having a garbage collector always running behind me cleaning up my memory poops. Even though Rust can end up with some ugly/crazy code, I love it overall because I can feel pretty safe that I'm not going to create hard-to-find memory errors. Sure, I can (and do) write code that causes my (rust) app to crash, but so far they've all been super trivial errors to debug and fix. I haven't tried Zig yet though. Does it give me all the same compile time memory usage guarantees?
- seemaze 1y agoAt first, the 12 year old inside me giggled at the thought of 'memory poops', but then I realized that a garbage collector is much more analogous to a waste water treatment plant than a garbage truck and a landfill..
- mattwilsonn888 1y agoAs long as the audience accepts the framing that ergonomics doesn't matter because it can't be quantified, the hand-waving exemplified above will confound. "This chair is guaranteed not to collapse out from under you. It might be a little less comfortable and a little heavier, but most athletic people get used to that and don't even notice!" Let's quote the article: > I’d say as it currently stands Rust has poor developer ergonomics but produces memory safe software, whereas Zig has good developer ergonomics and allows me to produce memory safe software with a bit of discipline. The Rust community should be upfront about this tradeoff - it's a universal tradeoff, that is: Safety is less ergonomic. It's true when you ride a skateboard with a helmet on, it's true when you program, it's true for sex. Instead you see a lot of arguments with anecdotal or indeterminate language. "Most people [that I talk to] don't seem to have much trouble unless they're less experienced." It's an amazing piece of rhetoric. In one sentence the ergonomic argument has been dismissed by denying subjectivity exists or matters and then implying that those who disagree are stupid.
- jasonpeacock 1y ago> produce memory safe software with a bit of discipline "a bit of discipline" is doing a lot of work here. "Just don't write (memory) bugs!" hasn't produced (memory) safe C, and they've been trying for 50yrs. The best practices have been to bolt on analyzers and strict "best practice" standards to enforce what should be part of the language. You're either writing in Rust, or you're writing in something else + using extra tools to try and achieve the same result as Rust.
- tptacek 1y agoI actively dislike Zig's memory safety story, but this isn't a real argument until you can start showing real vulnerabilities --- not models --- that exploit the gap in rigor between the two languages. Both Zig and Rust are a step function in safety past C; it is not a given that Rust is that from Zig, or that that next step matters in practice the way the one from C does.
- dadrian 1y ago
- ajross 1y ago> Seasoned Rust coders don’t spend time fighting the borrow checker - their code is already written in a way that just works. That hasn't been my experience at all. At best, the first version of code pops out quickly and cleanly because the author knows the appropriate idiom to choose. Refactoring rust code to handle changes in that allocation idiom is extremely expensive, even for the most seasoned developers. Case in point: > Once you’ve been using Rust for a while, you don’t have to “restructure” your code to please the borrow checker, because you’ve already thought about “oh, these two variables need to be mutated concurrently, so I’ll store them separately”. Which fails to handle "these two variables didn't need to be mutated concurrently, but now they do".
- mattwilsonn888 1y agoI would be interested to read the debates that stems from this point.
- simonask 1y agoThis is an interesting comment, because you point directly at the exact reason Rust’s approach is so productive. In the C/C++/Zig code, you would add the second concurrent access, and then start fixing things up and restructuring things - if you, the programmer, knew about the first access, and knew that the concurrent access is a problem. In countless cases, that work would not be done, and I cannot blame any of the involved people, because managing that kind of detailed complexity over the lifespan of a project is not humanly possible. The result is another concurrency bug, meaning UB in production. Having the compiler tell you about such problems up front, exactly when they happen, is a complete game changer.
- ajross 1y ago> In the C/C++/Zig code, you would add the second concurrent access, and then start fixing things up and restructuring things Well, sure, which in practice means throwing a lock around it. I mean, I get it. There's a category of bugs that happen in real code. Rust chooses some categories of bugs (certainly not all of them) to construct walls around and forces code into idioms that can be provably correct for at least a subset[1] of the bug space. For the case of memory safety, that's a really pretty convincing case. Other areas are less clear; in particular I'm not a fan of Rust's idea of threadsafety and don't think it fits what actually performance-parallel code needs. [1] You can 100% write racy code with Sync/Send! You can write racy code with boring filesystem access using 100% safe rust (or 1980's csh code, whatever), too. Race conditions are inherent to concurrency. Sync/Send just protect memory access, they do nothing to address semantic/state bugs.
- aiono 1y agoA lot of criticism of Rust (not denying that there are also a lot useful criticisms of Rust out there) boils down to "it requires me to think/train in a different way than I used to, therefore it's hard" and goes on to how the other way is easier which is not the case but it's just familiar to them hence they think it's easier and simpler. More people should watch the talk "Simple made easy" https://www.youtube.com/watch?v=SxdOUGdseq4 https://www.youtube.com/watch?v=SxdOUGdseq4
- bsder 1y ago> Seasoned Rust coders don’t spend time fighting the borrow checker - their code is already written in a way that just works. I really wish people would quit bleating on about the borrow checker. As someone who does systems programming, that's not the problem with Rust. Which Trait do I need to do X? Where is Trait Y and who has my destructor? How am I supposed to refactor this closure into a function? Sigh, I have to wrap yet another object as a newtype because of the Orphan Rule. Ah yes, an eight deep chain initialization calls because Rust won't do named/optional function arguments. Oh, great, the bug is inside a macro--well, there goes at least one full day. Ah, an entity component systems that treats indices like pointers but without the help of the compiler so I can index into the void and scribble over everything--but, hey, it's memory safe and won't segfault (erm, there is a reason why C programmers groan when they get a stack/heap smasher bug).