I hope it is not overly complicated to use Rust as the article suggests

I was on the xi (Rust-written texteditor) forum page and I saw this: Why I rewrote my Rust keyboard firmware in Zig: consistency, mastery, and fun

So apparently somebody claims that they could write some keyboard firmware is much easier in Zig compared to Rust. Please tell me that the author of this is actually some how mistaken and missed something about Rust? I hope it is not this difficult to write Rust code as the author claims?

Disclaimer: I am not trying to promote any language, I am just asking for your honest opinion as to how accurate this article is, that is all

I mean, it is well known that Rust is more difficult to learn than many other languages, but that article is a pretty poor description of what people usually find difficult. Especially the issue with their pins being different types has several workarounds that are less hacky than that macro stuff.

7 Likes

Where I have seen people give up is based on their experience with the borrow checker and/or perceived complexity of dependencies (can’t find xyz pain points).

The article talks about the challenges associated with the type system; perhaps “overly fragmented” might be a way to capture the essence of it.

While there has been a lot of ergonomic improvements that impact “ease of use” while remaining true to the rules, I agree what is missing from strongly typed languages is more documentation on how to unify your types. There are lots of options but none that I have found in a comprehensive, yet concise manner.

This may be a gap... closing the loop.

2 Likes

Some random thoughts, feelings...

If someone has created a systems programming language that is as type-safe, memory-safe and generally focused on correctness as Rust but made it simpler to use then all power to them.

To me Rust is a large and complex language. After 18 months of using Rust I probably don't know the half of it. I have (almost) never created anything with generics or macros, or that demanded complicated lifetime annotations or "where" clauses.

I still falter and scratch my head trying to comprehend the new fangled functional style I see pass through here everyday. I don't understand half the questions asked here, nor the answers.

That has not stopped me. Most of what I write looks pretty much like Javascript at the end of the day, or at worst C.

I must admit, the most I have done with Rust w.r.t down'n'dirty tweaking processor registers is a few experiments with memory-mapped registers on a Raspberry Pi. Did not seem so hard.

8 Likes

It seems to be genuinely a case where Zig fits better the task/approach that the post author wanted.

I think the Rust version could have been designed better. The problem in this article came from trying to use #ifdef-like approach of putting multiple different implementations in the same functions, with heavy use of #[cfg]. That's not a nice approach, especially when working with strictly-typed libraries.

Rust version could have been more elegant if it used traits like ReadKeys that could be implemented for each type of hardware independently, without lots of #[cfg].

It also seems that Zig's libraries for different keyboards happen to have same API/types, and Rust equivalents had different APIs/types. So in Zig the libraries have already abstracted away (or never handled) the hardware differences that the Rust version had to deal with.

4 Likes

Ah, there we go then. That is cheating. An unfair comparison.

I wouldn't call that cheating. Ensuring correctness via type system is just a different approach, with its own trade-offs.

No, I thought you meant that the Zig code presented in the story was using some unshown libraries that already abstracted away the differences in peripherals where as the Rust code was from the ground up.

I should go back and read it properly.

Anyway, don't care. I'm here now. Having used, extensively, more languages than I ever wanted to or was sensible I absolutely refuse to even consider yet another one. It's Rust, Javascript and C till the end of my days.

It's just their personal experience. I have a different experience with Zig. My first Rust project (after some simple toy apps) was the fltk bindings. I toyed with Zig a little, worked great, so I thought I'll write fltk bindings in Zig (I mean it should be a breeze, I've already made the C bindings):

The experience was quite painful, I ran into about 10 Zig bugs, reported 2 (they were fixed in master), the others were already reported and still open. I voiced my concern regarding one of them in the Zig discord since it was a blocker for platform support (Zig ships different version of libc (musl and glibc), however it can build against a different version than the system installed one leading to link issues).
Another blocker which I complained about was that Zig only does syntactic checking of code that isn't used. It's considered a feature not a bug. Obviously this can blow up when someone uses a faulty function and is a maintenance nightmare.
It also lacks things like macros or interfaces, making it painful to wrap something like a gui library. It also has many builtin functions, which should either be functions or made into keywords. The most painful is the casting story which has many builtins that are incompatible and you would still need to know which to use and when. The comptime T also has the same non-constrained problem of C++ templates which can also blow up error messages (which might depending on the whim it seems of the compiler be truncated to much fewer messages, so you don't know what exactly blew up in your generic code).

I know Zig is still at 0.7 or 0.8 and I'm sure things will improve with time. Anyways my first foray with Rust has been a lot easier than with Zig.

9 Likes

As always, your mileage may vary. However, on several occasions when I see people complain about how hard Rust is, it usually boils down to one of the following problems:

  1. They have little or no experience with systems programming, and little or no knowledge of low-level computer architecture. The latter is especially inevitable for undestanding and internalizing concepts related to memory management. They are just as important to know when learning C or C++; this is not specific to Rust.

    However, Rust rewards one's lack of such knowledge with noisy compiler errors, while C and C++ silently misbehave instead. Some people claim that this makes C and C++ "easier" (to learn / use). I disagree with this because it's too simplistic; getting code to compile is just half of the story. The other half is debugging, and that's much more painful in other systems programming languages.

  2. Another problem might be that Rust uses concepts which are either completely novel or merely non-mainstream in other languages. Some programmers try to write Rust code by transliterating strictly Object-Oriented or strictly functional code, and that just doesn't fly. One must be willing to learn the idioms of the language, and complaining that Rust is not C or JavaScript or Haskell or Python is anything but productive.

  3. While the first two bullet points are the fault of the programmer, not that of the language, there are of course genuine deficiencies or inconveniences in Rust. For example, const generics is not yet stable or complete, and the handling of fixed-size arrays is consequently sometimes inconvenient. A lot of core functionality, such as random number generation or regular expressions, is not included in the standard library – which is by-design and for good reasons, but nevertheless, it can be annoying. Type inference and the borrow checker are sometimes a little dumb, in some edge cases rejecting code that is otherwise "obviously" valid to a human. These are legitimate concerns, and most of them are being worked on.

So, in brief, it is not the case that "Rust is hard". Rather, it is the case that it requires previous experience and exposure to some advanced concepts of computing, and it also doesn't let you get away with many mistakes. But that's not a bug, that's a feature – it is the very point of its existence.

17 Likes

Also see the relevant reddit discussion, @matthieum made some points there:

1 Like

Rust tries to focus on up-front correctness far more than other languages. This is a tradeoff -- just like other ones you may be familiar with, like static vs dynamic types.

You'll see the same thing in many places between languages. People used to compare Go and Rust frequently, but I don't think they're competing for the same people at all because of their different philosophies. If you share Pike's goals for a language, then Go is a great language. If you don't, then it probably makes you super frustrated.

Rust is mostly safety, so if one isn't worried about that, one will probably enjoy other languages more.

5 Likes

Perhaps someone could answer a simple question for me:

Is Zig a type-safe, memory-safe, thread-safe, language?

If not then we are not comparing like with like.

If Zig is "easier" by some criteria that is great. But if it sacrifices the above on the way then it is designed with different goals and values in mind. In the same way a dump truck is not a Formula 1 car.

Maybe it's possible but I don't see how Rust can do what it does without being the way it is. Hard or not, that is what it takes. Being the first of it's kind I guess nobody else has thought of a way to do it either (Correct me if I'm wrong).

The only language I know that is in the same game, designed with the same values in mind, is Ada. Which is also not the easiest of languages to get on with.

3 Likes

No. Not in the Rust sense at least. It offers some runtime checks when building in Debug or ReleaseSafe modes. Thread sanitization was supposed to land (not sure if it did or not yet). The idea is that it reduces the complexity of the language, while enforcing more safety than C or C++. So it does compare to Rust in the "safer" systems programming domain.

The language's overall goal is solid, and the resulting code is nice. The laxer mode of the language also means less burden on library designers but more on application writers. Wrapping C libs is also eaiser (translate-c is incorporated into the build system, no need for crates like bindgen for example).

That said, Zig is getting more complex quickly with its metaprogramming capabilities, builtins, null-terminated strings and async model. The compiler is also becoming more bloated and brittle with the bundling of multiple different versions of libc and integrating the build system in the compiler (more points of failure). I also believe the lazy evaluation feature will come back to bite at some point.

2 Likes

Ah thanks.

Thing is, to my mind, only "some checks" and "at runtime" is kind of easy and makes it a very different thing than Rust.

Rust strives to check everything and as much as possible at compile time. Which as far as I can tell necessarily has an impact on the syntax and semantics which some perceive as "hard".

Apparently Zig is prepared to give up a lot of that for apparent simplicity of coding.

I make no comment as to whether that is good or bad. It's just a different set of values in play.

6 Likes

Reading the article really struck me. Not only did I start with Rust about the same time, I too have been experimenting with Rust embedded for the first time recently, and had similar struggles.

However, I didn't attribute my difficulties to "features of the Rust language", but the specific way they were used -- particularly the design of svd2rust's auto-generated crates and its interaction with embedded-hal's traits.

I was trying to bring up an unknown-to-crates-io dev board, and get it to "blink hello world". There was a HAL crate for the microcontroller, at least, which made this seem not very daunting. I planned to follow The Discovery Book, tweaking as I went along, and sail smoothly through it.

As I had hoped, the first example compiled and booted on the board, but did nothing obvious (since the pins don't match, of course). But my attempt to make my own separate crate with similar-looking code turned into a sea of type errors and messy imports.

The example code was extremely readable, but was relying on a huge artifice. In addition to the example having an auxiliary file (which it took me embarrassingly long to figure out) the types full of generics and traits were in complex relationships not obvious at all from reading their cargo docs.

For example, I hit his specific complaint about all pins appearing to be different types, when I wanted to look at them as a collection. It took me over two hours to work out that:

  • the GPIO fields of the Peripherals struct are members which implement RegisterBlock and are themselves unique structs
  • all of them have members which are unique types corresponding to pins, with generic subtypes
  • the subtypes are defined in this crate, but they all implement the Pin trait despite their generic-ness
  • through some other trait I couldn't quite figure out, you can get a Writer to the Pin part
  • that lets you access pin functions, whose return types are generic but change with based on what modes you set
  • and in the end, that means and means you can split() them into a collection of the same type, which is the non-generic Pin from embedded-hal.

And I'm pretty sure I still didn't quite describe it correctly!

Even though I knew conceptually how embedded-hal (and its ecosystem) was designed, and I was looking at example code for a very similar processor, it was the relationships between the dozens of items and traits across multiple crates I had a lot of trouble with. This "abstraction heirarchy" is something that per-item cargo docs without any top-level comments are not very helpful for, and I point the finger at svd2rust for that. (EDIT: I don't mean to imply it's easy to fix, only that I think it was the cause.)

However, this was still Rust in the end. So once I figured it out, it Compiled And Ran Perfectly(tm). Now I could start on implementing peripherals (using the generics without worrying about the details nearly as much), and maybe even a full board crate someday.

I would certainly have never looked at another language. Most of the time, the precision and verbosity is exactly what I like about Rust, since most type abstractions don't get this deep.

I will not speculate on Zig the way he does on Rust's history at the bottom, but reading the Zig examples and their website does not make me feel like I could "be productive in Zig without an internet connection." Particularly that break :x res catch unreachable; on their home page. :thinking:

In conclusion, @Joe232 and other readers, that is definitely one guy's opinion.

EDIT 2: The crate I believe generated the base processor crate was fixed. No scalable vector graphics here! :laughing:

7 Likes

This is something I have been putting off for a long while now. I have an STM32 micro-controller board here. Actually I have a few different ARM based micro-controller boards. So obviously I though of trying my hand at Rust on them.

However, after looking into the Rust support for such things one soon runs into what seems to be the huge and and complex world of HALs, auto generated interfaces, peripheral register description files and whatever else. As you describe.

I immediately dropped the idea. Or at least postponed it. Past experience has taught me that such things are great when they work but can take forever to understand and adjust for some new variant. When all I want to do is bang on a register or two at some address that I know with some bits that I know and make a LED flash. On a Propeller micro-controller for example that is a handful of trivially obvious code!

Of course the real problem here is the amazingly complex world of peripherals on micro-controllers. Hundreds of registers, thousands of bits each with their one meaning, all kind of strange interactions between them. When one wants to handle all of those on a chip and abstract over all kinds of chips the HAL things will necessarily become very complex.

Perhaps I should take another look. No doubt things have evolved, documentation improved and so on.

3 Likes

I would agree that a significant part of the complexity is reflecting the problem domain. In fact, I would lay out the overlapping problems I had this way:

  1. This board is my first ARM-based MC. There was a lot of architecture knowledge I didn't know which was key to the interface. For example, in order to do delays, you needed a Clock struct. For most Cortex Ms, you have to configure the PLL for the oscillator and then freeze() it to get one. It makes perfect sense -- if you already know that's how the chip works. I'm used to putting in the external oscillator frequency as a constant, and doing all the calculations from there.
  2. The auto-generated items are based on the names in SVD. This is great if you have the manual sitting in front of you, or you are experienced with the chip family. If you don't, it's a struggle. Sure I can guess that GPIOA is Port A for a bank of GPIO pins, but what the heck is BTR ACCMOD_R? You have to look around elsewhere in that submodule and piece it together yourself.
  3. There are no module-level comments. This is what I would like to see improved in the svd2rust package. Even something so simple as tying the R and W structs to the structs of Pins they control. There are a couple of cross-links, like to the Read trait in general, but not where the registers "come from".

None of these problems (referring back to the OP) have to do with the Rust language itself. In fact, now that I have a blinky lights example, and have learned that, I think I have a general instinct on how to search the documentation for registers, structs, and what is attached to what if I find something in a specific module path. And I can proceed to greater things, with the compiler at my side. It was worth it.

I hope you will try it someday, @zicog. Just make sure you have coffee first! :grinning_face_with_smiling_eyes:

I imagine people who routinely use embedded-hal developed this documentation sense many years ago -- or if they use closely-related MCs in the same family, just memorized things or have their own example code. But for an occasional tinkerer? It's a sheer cliff of a learning curve to start unless you have one of the specific boards that is a well-worn path.

I sincerely hope the maintainers of svd2rust can improve their cross references. That would help a lot.

1 Like

OK, what is ACCMOD_R?

Out of curiosity I asked google which only lead me to Rust related documentation, which did not say anything useful.

I guess it's all obvious if you are familiar with all the thousands of registers in your SoC and documents like this: https://www.st.com/resource/en/reference_manual/dm00043574-stm32f303xb-c-d-e-stm32f303x6-8-stm32f328x8-stm32f358xc-stm32f398xe-advanced-arm-based-mcus-stmicroelectronics.pdf

Hey, it's only a thousand or so pages !

stm32f4xx_hal::stm32::fmc::btr::ACCMOD_R means:

  • fmc = Flexible Memory Controller
  • btr= (b-something) timing register
  • ACCMOD_R = access mode

Gotta love those unnecessarily abbreviations. It's like they needed to make their variable names smaller so it'll fit into their MC's memory /s