Live data from Hacker News

Assorted Thoughts on Zig and Rust

scattered-thoughts.net

281–290 of 307 posts

Re: Assorted Thoughts on Zig and Rust

#281
post #168

> Zig is dramatically simpler than rust... Most of this difference is not related to lifetimes. I've been thinking about this recently: the borrow checker gets a lot of attention, but I think the majority of the learning curve of rust is actually due to "unforced errors" in the language UX which have nothing to do with the language's core USP's. For instance, the module system just seems needlessly complex. Like you…

Another large category of language UX issues is, ironically, the sugar that gets applied to the syntax. The classic example, but far from the only example, is auto-dereferencing: https://stackoverflow.com/questions/28519997/what-are-rusts-... Rust has many syntax features that can be described as "Rust takes some common pattern that's onerous to write and read, and automatically infers it for you under certain condit…

Blog post or not, it could definitely benefit from some examples.

Re: Assorted Thoughts on Zig and Rust

#282
post #120

Earlier quoted context omitted.

> there is no feature in Rust to force a function to not be removed Can we use `pub` to make it public and not to be removed ?

no I think I tried just about everything: pub, extern etc. The only thing that worked was adding a linker argumment: "-C", "link_args=--undefined= " That will force the linker to assume it's referenced.

If the function you trying to keep is in non-pub module, it will be removed. Maybe you could move it out of private module: https://rust.godbolt.org/z/PYPbG8>.

There's also an `#[used]`[1] attribute, but only for static items. If your use case is special, consider open a feature request in Rust repo.

[1]: https://doc.rust-lang.org/nightly/reference/abi.html#the-use...

Re: Assorted Thoughts on Zig and Rust

#283
post #265

I believe if you write tests hooking up your tests with the test allocator will effectively prevent all blatant UAF and memory leak events. More subtle ones that happen due to spooky action at a distance and wierd incomposability might be out of reach. (Not at op, who does write tests:) You are writing tests, right? ;)

Testing is a good way to ensure that your program won't have UAF under most normal circumstances. But when it comes to security it's adversarial - your program will get pushed into parts of the state space that were never seen during testing. Things like browsers and operating systems are all heavily tested and fuzzed using tools like asan. They still have security issues from UAF.

if you're worried about errors coming in through testing and fuzzing, you could be just as in trouble in a language as rust due to, say an unsafe block not composing well with another unsafe block two dependencies over. The challenge then is to figure out how to debug, I would worry that obsession with "zero-cost abstractions" making your code difficult to reason about and obscure the bug more than a system that has a more barebones relationship with the computational processes. However, only time will tell which is the better strategy.

Re: Assorted Thoughts on Zig and Rust

#284

Earlier quoted context omitted.

I've mostly seen it done in situations that are roughly analogous to IoC containers. So, custom serde libraries that use reflection, testing utility code, object mapping tools, magic validators, stuff like that. Which, any sufficiently venerable enterprise Java application seems to have at least one or two of those knocking around the codebase. FWIW, Java is also where I see stringly typed designs, too. I'm increasin…

Right, for DI/IoC it seems pretty standard. (I think it's probably a historical accident, honestly. Reflection was really the tool to get constructor parameters and then someone figured, hey, why not just inject private values directy? It surely convenient at the time, but... lessons learned, I guess. I have no problem with compile-time DI via static reflection of e.g. constructors. It's a little more boilerplate, bu…

This is how I read it:

> I'm increasingly coming to fear that languages with more safety-oriented features are[] associated with safer practices [not] because those features encourage safer design, [but] because they tend not to attract programmers with a swashbuckling attitude in the first place.

Re: Assorted Thoughts on Zig and Rust

#285

Earlier quoted context omitted.

I thought this way too until I saw a bunch of people praising how easy the module system was because they felt it was very similar to Python's. I don't know Python well so I can't speak to it. My working theory: each language does modules in a different way. People think "Oh, a module system, I know this" and then run into issues when it works differently than their language. Just a theory though, I have not been abl…

Honestly, that idea makes a lot of sense to me, and personally, I find the module system pretty normal seeming for a modern language. I'm used to Perl and CPAN, and it's pretty similar to that, except for the option to use dir/mod.rs, which honestly seems like it's kinda nice for keeping a module contained nicely for those that want to do it that way. Now I'm more interested in what the complaints about it are, and s…

So a point of comparison would be like a Swift or a Java where module/package definitions are defined implicitly, based on the location in the file system. In contrast, Rust's explicit module declarations seem cumbersome and unnecessary, especially since 90% of the time you're just typing out a structure which is identical to what already exists in the file system. So this could easily be inferred, but it's just not.

I think the same can be said for match statements on enums: in Rust you need to match against the fully qualified type name, when in other languages (including Zig) you can infer everything up to the enum case, since it's already specified by the type you are matching against.

These kinds of things just seem weirdly inconsistent, since in many cases Rust favors inference and elision to remove boilerplate, while in other cases it requires explicitness when there is no technical reason for it to be required, and other languages handle inference just fine.

Re: Assorted Thoughts on Zig and Rust

#286
post #259

Earlier quoted context omitted.

> but fortunately it's also perfectly well-defined by the platform. that's the same for every language and thus not very relevant. if you have single, static binaries / libraries of course everything is simple, and you'll get linker errors in C++ just like you would in Rust. What is not simple is when you start loading twelve dozen libs at load-time or run-time and it does not seem that Rust defines behaviour any mor…

Right, I've been telling you it's not relevant for the past three comments now. You seem to be under the impression that dlopen somehow interacts with language undefined behavior; it does not in either Rust or C++.

> Right, I've been telling you it's not relevant for the past three comments now.

but it is ! the only reason why ODR is UB in C++ is because the C++ language authors can't force the system linkers (again, whether at link time, load time or runtime, I'm not only referring to dlopen) to perform LTO which trivially makes ODR violations a diagnosticable error.

But as far as I know, neither can the Rust language authors do so - so either the behaviour in Rust is as defined as in C++, or Rust does not support creating standard platform object files that are linked by ld, gold, or whatever (such as D, ADA, Fortran etc all support) which would make a fair amount of use cases impossible - it's pretty common in some HPC circles to link C++ and Fortran directly in the same executable for instance. And then, of course it's easier to define behaviour when you use a reduced set of constraints, but it definitely does not makes something worth bragging about.

Re: Assorted Thoughts on Zig and Rust

#287
What do Zig "generics"/"comptime" errors look like ?

> One of the key differences between zig and rust is that when writing a generic function, rust will prove that the function is type-safe for every possible value of the generic parameters. Zig will prove that the function is type-safe only for each parameter that you actually call the function with. On the one hand, this allows zig to make use of arbitrary compile-time logic [...]

This is fundamentally identical to how C++ templates, constexpr and concepts work. Its a really flexible system (you can implement how you want to type check things using constexpr), but has three cons that Rust system does not have:

- can't typecheck library APIs, so library authors aren't sure if their "constraints" are correct. Testing this requires writing lots and lots of compile-time tests.

- errors deep inside a library implementation when user code passes incorrect arguments to generic APIs.

- rust traits can be used for static dispatch, or boxed and used for dynamic dispatch, C++ at least can't really do this well.

It would be cool if someone could explain how Zig fixes or improves upon these problems that this system has in C++. C++ tried to fix this with concepts, but failed.

This was a nice read that has motivated me to learn Zig. I want to know how Zig improves on these C++ issues.

Re: Assorted Thoughts on Zig and Rust

#288

I think Zig's biggest advantage is that it's just C without the warts, or footguns as is said in the Zig world. The comptime feature is probably the most exciting thing I've seen in a while. I've looked at Rust and feel it's more of a competitor to C++, Java and C#. Whereas Zig is C done right.

> The comptime feature is probably the most exciting thing I've seen in a while Just please do not make the mistake of believing that it is unique to Zig. Factor brings the best of Forth and Lisp together, so meta-programming or extending the language is possible quite easily, for example. You could extend the syntax or add constructs pretty easily, and so forth. Anyways, an example can be found here: https://rosetta…

> The comptime feature is probably the most exciting thing I've seen in a while

Also if comptime is simply compile time evaluation / execution, C++ has it with constexpr and templates.

Nim has this too I think.

Though judging by your comment not as powerful as Factor (I'm not familiar with it) extending syntax etc.

circle C++ probably gets closer: https://www.circle-lang.org/

Re: Assorted Thoughts on Zig and Rust

#289

This is a really great writeup! I was using Rust as my main programming language from some months before 1.0 up until maybe early 2019. I have only written somewhere in between 100 and 1k lines of Zig, but generally feel that I agree with most of what's being brought up here. Here's a mind dump: > Zig manages to provide many of the same features with a single mechanism - compile-time execution of regular zig code. Th…

> > compile-time execution of regular zig code The problem with this is that it's incompatible with a well-designed compiled[0] programming language; a cross compiler can't replicate the architecture-specific behaviour of the compiled code because the target architecture is unavailable or possibly even nonexistent at the place and time the compiler runs. Consider, eg, a compiler on ARM targeting x86, when the code us…

> a cross compiler can't replicate the architecture-specific behaviour of the compiled code because the target architecture is unavailable or possibly even nonexistent at the place and time the compiler runs.

I mean, it can't possibly be nonexistent when you write the program, since if you don't have any idea what the target looks like, how can you output code for it? Do you mean physically existent?

I think I see the overall point though, namely that arch specific code would be a mess, with which I agree. But isn't this okay? I mean, if you really want to switch on some platform specific thing at compile time, then different behavior for different host platforms is exactly what you want.

To me, there's basically two uses of compile time execution: precomputation and codegen. If we look at the following snippet

  const std = @import("std");
  pub fn main() anyerror!void {
      std.debug.warn("{}\n", .{.{
          @sizeOf(*usize),
          comptime @sizeOf(*usize)
      }});
  }
we're looking at the size of a `usize`, which is pointer-sized, and we're taking it both with and without `comptime`. If I compile and run this on my system, it prints out 8 and 8, since I'm on a 64-bit system.

  /h/m/t/zig$ zig build run
  struct:5:21{ .0 = 8, .1 = 8 }
However, Zig does support cross compilation, and I can compile this for 32-bit mode, and run it, which then gives me 4 and 4.

  /h/m/t/zig$ zig build run -Dtarget=i386-linux-musl
  struct:5:21{ .0 = 4, .1 = 4 }
It's worth noting that the docs for `@sizeOf` says "This function returns the number of bytes it takes to store T in memory. The result is a target-specific compile time constant.".

> you a: need two different codegens

Won't you need `n` different codegens if you support cross compilation to `n` different targets anyways?

Re: Assorted Thoughts on Zig and Rust

#290

Earlier quoted context omitted.

> doing compile-time execution in a sound, safe way is not simple. For example, cross compilation becomes more of a thing. It is easy to accidentally break the type system. Why? I don't think I've ever seen a concrete example of why this isn't simple (but I'm not really a compiler person, so there might be!). I can imagine plenty of ways of doing Bad Things, like adding compiler flags based on what day it is, but I c…

> Accidentally getting non-typechecked code in the compiler, or running into problems with the compiler thinking two equal types are distinct? Yes, this sort of thing. Basically, you have to have this be deterministic , or you end up with very strange possibilities, possible miscompilations, and in the best case, confusing errors. One option is to simply accept that these things can happen. Another is to restrict wha…

> An extremely simple example is cross compiling. In Rust, usize is dependent on the architecture you're compiling for. A very simple "just compile and run the program, get the answer, and use it" implementation of compile-time execution will produce a usize of the size of the host, not the target.

I get that there are non-obvious problems here, and as you say, this problem specifically has an easy fix, but I'd just like to note how Zig does this:

  const std = @import("std");
  pub fn main() anyerror!void {
      std.debug.warn("{}\n", .{.{
          @sizeOf(*usize),
          comptime @sizeOf(*usize)
      }});
  }
Modulo the `.{` weirdness, this probably looks familiar. By default, this prints out 8 and 8 on my system, but if I cross-compile to a 32-bit target, it prints 4 and 4.

> It's just that, when you start applying this super rigorously, you end up in weird places. How can you trust any behavior in a language without a specification?

I think this is a social issue for me; if rustc decides one day to change its behavior under my feet it feels like it's my fault for not having written proper Rust in the first place (even if the behavior wasn't properly defined in the first place), but if there's crazy things going on in the compiler due to bugs (that we didn't find, because proofs are hard), then that's not really my fault, in a sense. And of course, if my CPU decides to run my program wrong, that can't really be blamed on me. The end result in these three cases are all the same: the program didn't run as expected, but the blame (I don't want to point fingers, but this is the best word I could come up with) is different, and the probability of this happening is vastly different. I've never hit a CPU bug, but in the little unsafe Rust code I have written, I've had behavior change with a compiler update, which I'm sure is because I hit UB.

And for what it's worth, I would greatly prefer Rust having a proper spec, even if that would increase turnaround time for the language evolution, just to ensure that everyone really is on the same page with respect to what the language really should and shouldn't do. I realize that Rust would rather be careful and make sure that the decisions made are the right ones. I think it's a fair trade-off, but I'm not sure I would have made it, if it were up to me.

Post reply on HN