The same program in C, Rust and Zig

Once I’m ready I intend to put this little experiment up as a blog post, which I’ll link here. I do a fair bit with MCU’s and often integrate them into my audio related hardware projects. A common need in my guitar amplifiers and effects is an lfo signal, used for effects like tremolo, vibrato and phasing. I’ve done all analog circuits but find that going with an MCU brings a -lot- more flexibility, as I can switch waveforms and give a much wider speed range. For this, I often resort to a sine lookup table rather than generating a sine wave on the fly.

I wrote the original in C quite a while ago. It’s a typical small C commandline utility, which uses getopt for argument parsing and gives you a LUT that’s ready to copy from the terminal and paste right into a C or Arduino file as an array of ints. When I was learning Rust last year I decided that porting this little program would be a good way to dive right in with a program that I already understood, which was useful to me, and was not a huge task. Yesterday I decided to have a go with Zig on the same task.

The criteria were that the programs would use only the tooling that comes bundled with the language distribution, but could use libraries if needed. An attempt would be made to make the code idiomatic, without any attempt at super optimizing things, but mainly it should represent what the average programmer would knock together quickly to solve the problem. This is a tiny little command line utility that does it’s business and exits, so speed comparison is not really part of the criteria.

Additionally, for the Zig version I wanted to see how closely I could follow the design of the Rust program. It turns out I did an almost straight up translation.

  • I used clap-rs and zig-clap respectively for argument parsing
  • The parameters are saved in a Specs struct
  • The business of calculating each position in the LUT happens in a method of Specs
    Some differences between the two:
  • Zig has an extra usage function, where clap-rs basically pops up a nice little statement telling you that you have passed an invalid argument and that you can try rslt --help for more information if you try to feed it garbage. Clap-rs is a more turnkey solution. Not disparaging zig-clap, it does most of what clap-rs does but you just have to actually tell it to do it.
  • Rust’s for loop can iterate from 0 to the specified length quite succinctly. Zig does so with a while loop, which is almost identical in syntax to how I would (and in fact did) do it in C.

Now the fun part, comparing results.

The C version comes in at 16k without doing anything special and links dynamically to libc and libm. There’s no long options and for consistency with the ecosystem I wrote a manpage for it and run the build from a Makefile. It’s all skills that I already had, but I know that not very many people write manpages from scratch. Or Makefiles for that matter. The C version is the shortest in terms of LOC at 84 lines. I could cross compile this with one of the cross toolchains that I’ve already had built for a while. Or with zig-cc!

% zig cc --target=riscv64-linux-musl -o cslt cslt.c
% file cslt
cslt: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), statically linked, not stripped

The binary is statically linked to musl libc and weighs in at 56k. Not bad at all.

The Rust version comes in, in debug mode, at a whopping 13M. I still love Rust but this sort of thing has always bothered me. Just by pulling in clap, cargo pulls in a total of 25 crates, all of which get compiled statically into the binary. After tweaking the release profile and compiling in release mode we get 632k. The source code is 105 LOC. There’s nothing special in the code and I’m not using anything particularly advanced. In fact, what’s here is really a tiny subset of Rust’s features, which is something that bears mention whenever people complain about the scope of the Rust language - you can get by in quite a lot of cases just using a subset of what is provided. I’ve written some really complex gui programs without ever touching lifetimes or async, and only have one instance of unsafe in all of my Rust code (not counting dependent crates).

I can cross compile this for musl without too much trouble by using rustup to add the target first, but in order to compile for a different architecture I need to install a cross linker and tell the toolchain where to find it. It’s better than C before we had zig cc but still a WIP.

The Zig binary comes in at 144k in release-safe mode and 60k in release-small, and of course is a fully static binary. This is in keeping with the statically compiled C version linked against Musl, plus it provides loong options and a better usage statement. Just what I expected really. The code is exactly the same length in terms of LOC as the Rust version, but could be smaller had I chosen to emulate the design of the C version. However, the C version uses global variables as a shortcut, which is something that I won’t do on larger projects, and this design is more where I would be going for anything beyond a small utility like this.

The benefits of both Cargo and build.zig are readily apparent, as are the self documenting features of both clap-rs and zig-clap. I don’t need a man page or a Makefile for either the Rust or Zig versions. For larger projects Rust provides similar functionality as Zig in that you can use a build.rs file as a build runner. However, I like Zig’s approach better, because build.zig is the only programmer facing interface to the build system, whereas you configure your build in Rust with Cargo.toml and then add a build.rs if you need things not provided by cargo.

Cross compiling the Zig version is the best experience of the three. In fact I was able to compile to arm, transfer the binary over to my phone, fire up tmux and run it there.

zig build -Drelease-small=true -Dtarget=arm-linux-musl

That’s literally all it took. No Android SDK to install, no cross toolchain or libraries. I love this, and it was one of the selling points for me when I first read about Zig.

I could have made life slightly easier by using gyro or another package manager, but wanted to stick with just the standard tooling. So I just included the zig-clap source in the source directory. That adds some overhead to distribution, but I know that down the road it won’t be neccessary when we have an official package manager in Zig.

I wouldn’t consider this a comprehensive comparison by any means, and I wouldn’t draw too many conclusions from it. I still use Rust, and I do I think some pretty cool things with it. It’s got what I consider best in class documentation provided you are willing to accept viewing it in a browser, a growing and already powerful ecosystem, an active community and has achieved the kind of critical mass that makes me comfortable saying it’s not going away for a long time.

Zig in contrast has a great start on many of those things but feels noticably incomplete in comparison, particularly with the std library documentation and lack of an official package manager. Both languages feel modern compared with C, and both languages encourage good practices compared with C. For something like this, which isn’t going to be a long lived program, Rust’s memory safety guarantees aren’t really that big of a selling point. You generally know if you’ve accessed memory inappropriately by getting a segfault or garbage output, and if it leaks memory anywhere it at least isn’t going to leak for very long. However with Zig’s design making allocations so obvious, you should be flogged if you forget to de-allocate.

The repo is at https://gitlab.com/jeang3nie/slt

12 Likes

Thank you for sharing your experience, it seems you did a great job at evaluating the different languages and I think your conclusion is perfectly reasonable.

As a general comment, not meant to be a rebuttal to anything you said, Zig is much younger compared to Rust and we don’t have the support of a big corporation behind us, but things are going well to the point that sometimes people forget that, see some things being very good (eg crosscompilation) and mistake other incomplete parts of the ecosystem for giant problems (eg docs), while in reality it’s just that the language needs time to grow and that things being incomplete should be the expected status, not the other way around.

Anyway, give a second look at Zig once we switch to the self-hosted implementation, it’s going to be a topical moment for the growth of the language.

3 Likes

Those are actually points that I intend to cover when I blog about it. And they’re isn’t really going to be a need for me to give it a second go as it were, because I fully intend to use zig regularly going forward.

I actually think that the state of the language is very impressive considering that until quite recently it was almost a one man show, the lack of corporate backing, etc. I already think that it’s a good C replacement, even before it fully matures.

And while I’m going to continue using Rust, I recognize some glaring issues with the language and ecosystem. The crate situation I mentioned earlier for one, I shouldn’t pull in 25 other crates for a command line parser. I also think Zig is ahead because the std library is independent of libc. The fact that Rust’s stdlib links to libc makes bootstrapping it for another platform nightmarish.

In fact, you still can’t natively compile rust code from RedoxOS, which is written in Rust. But you can compile C in Redox. Crazy.

I also think that they need a big asterisk next to their ‘zero cost abstractions’ tagline pointing to a disclaimer about how they abstracted away allocations and an allocation failure can cause a panic.

As for the state of Zig, I know that the documentation tooling is planned out and partially working already. I’m looking forward to the self hosted compiler, and as soon as Arch gets llvm12 in their repos I’ll be updating Zig again. I’m watching it closely.

4 Likes

You can always use the prebuilt zig from zlglang.org. I use the process from GitHub - fengb/zig-nightly
which creates pkgbuild for makepkg keeping pacman up to date

3 Likes

Thanks for that gem. I starred the repo.

Thanks to @Ed_T for the tip about installing the nightly compiler in Arch. I’ve updated the repo by making zig-clap a git submodule. To keep it in sync with the nightly compiler the zig-clap submodule needs to be kept on the zig-master branch.

While I was in there I decided, “Why not try this in Python?”, which I haven’t used much really ever, and then figured since I’ve been curious about Nim for a while I’d give that a shot as well. Might as well make it a bit more comprehensive. I was looking at maybe adding a few more languages, but I think I’ll stop and focus on finding something a little more challenging to do in Zig!