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
usagefunction, 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 --helpfor 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