Thank you, Maria, for the introduction.
I'm going to get right into it.
System programming.
Lessons from building a networking stack for microcontrollers.
I'm Patricia Wittingslow.
I'm the creative of El Netto, which is the networking stack that runs on these
constrained places with around 20 kilobytes of RAM.
And I'm a technical maintainer, a core maintainer, I'd say.
I teach Go at a university course, embedded systems with biomedicine applications,
and I've also taken consulting up recently.
All right, so this is the agenda for today.
The overarching question I'll try to answer is how Go can be used as a systems programming language.
When so many people say otherwise.
So, let's simple back.
Let's circle back to a simpler time.
I'm lay pain and picture.
It's mid-pandemic, and I'm hold over my desk,
hooking up wires, trying to connect the HTTP to an Arduino.
I've limited memory 32 kilobytes of RAM,
and I'm struggling to fit my CSS classes into the program.
I've decided to use the Arduino IDE for the last time.
There is no slide in.
Oh, yeah.
C++, you later.
No, that's on purpose.
Oh, yes.
It's all part of the...
Part of the...
Yes, suspense.
So, I desperately search on my browser.
Go for Arduino.
Time you go.
It seems a little too good to be true.
So, and it was at the time,
the LVM compiler, which time you go uses,
had series issues when targeting AVR technologies.
By chance, I had found time you go at the dawn of a golden age.
Soon after trying to put it to work for the first time and running into issues,
these issues were to be fixed by the LLVM team.
And I'd host my... Oh, this one should have a video, but...
Oh, it's...
That's...
But I think that's my fault.
Because Google sheet...
Oh, yeah, let's...
Oh, snap.
It's a screenshot they're saying here.
No, I can assure you it's not a screenshot.
Okay.
You're going to have to believe me that there's a video of...
Of...
Arduino...
...hoking up the HTTP.
And we're getting an LED light to blink.
So, yeah.
Let's hear for the Arduino.
All right, let me actually get the presenter view back on.
And soon after, we got that...
Arduino, you know, the blink.
Oh, and the CSS was terrible.
Just imagine terrible CSS.
So, that was the first attempt at a networking stack I did 4.5 years ago.
That's ether switch.
Second attempt was seeks.
Featured in the book, gained a lot of users.
Book kind of tiny go networking at a low level in the...
In the line light, as far as to say.
And the next generation stack, Alnetto.
That's the one we're working on today.
And the rest assured, Alnetto is likely definitive.
And the first...
Let me show you Alnetto in action.
And let's hope that the gods of the live-coding demos come up...
...company us.
So, I've already loaded the program into this...
...CYW449.
Since the...
...fastem network is kind of spotty.
I've...
I'm...
...all my hotspot.
Yes, it's on.
My hotspot is on.
You're going to go ahead and plug it in.
All right.
And...
So, I got here a website.
And you can't really see it.
I'm sorry.
I can't get to work for my laptop.
But I'm the person spotten.
And really hope that...
Oh, there you go.
The LED's starting on.
All right.
You get in that camera, a guy, person.
And it's not turning on.
No.
It's still turning on.
So, success.
Can you...
You can all see it, right?
Can I get a yes from the audience?
Yes.
All right.
Yes.
All right.
Feeling good.
So, that's doing DHCP.
And then hosting on the HTTP server,
which you see on my phone screen.
Now, my laptop screen.
So, today, there's no other way to run a fully-fledged networking stack...
on a microcontroller, other than with El Neto.
So, I think it'd be interesting to take a look at how we've got to hear.
Let's take a look at Ether Switch's design.
So, Ether Switch implemented these protocols from EtherNet up to HTTP.
And this was the first interface I came up with that drove the core idea of the networking stack.
Now, any reasonable person would look at that and say,
that seems reasonable enough.
That's because the reasonable person doesn't...
Didn't audit the code.
They don't know how I'm using this.
You see, a TCP connection would load the entire packet into memory.
And enter a loop that would call an update function.
That returned a new update function that was to be called.
Over and over until the packet was completed coded.
And the response also encoded.
And so, you might know there's something like,
to emit data, you needed to receive a packet.
To illustrate, if you, for some reason, need to send out two packets
after receiving a single packet, you wouldn't.
Won't packet in, won't packet out.
Then's the rules.
And this barely worked for an HTTP server.
An HTTP client on the other hand was out of the design scope entirely.
Now, this design was not by accident.
Turns out this shallow function calling would save on stack memory,
which was very limited on the Arduino Uno at that time.
Also, the one in one out decision was on purpose.
It effectively made the TCP stack stateless.
So, a lot of simplicity gains right there.
So, we have to keep track of accuracy numbers.
And the cost, 100% pedigree on maintainable code.
It may be the lightest on memory HTTP server in the world,
but we don't speak of others switch anymore.
All because I want to fit in those CSS styles.
Can you believe it?
But, you know, it did get some more CSS styles
for the Arduino library.
Let me in on.
So, some ones.
Next up, six.
And, or six, depending on your pruditionist.
It's supported NTP, DNS IPv6.
Got some, got something going on for it, you know.
And more support.
And with six came improvement.
The use of a singular is no accident.
Behold, my greatest failure to the go community.
I had spent another year and a half on the network in stack.
And all I could show for it was that I could now send a packet whenever I wanted.
I didn't have to wait for a packet.
And I could send two packets now, not just one.
And it's, in spite of its shortcomings,
seeks brought with it something new.
A became an overnight success in the embedded go community.
Since it was the only networking stack that enabled
the internet access on the Raspberry Pi Pickle W, people started using it.
And it's even covered in the book, home automation and go.
Seeing them now, people depended on it.
And how six was quite hard to maintain.
I set out to design a new networking stack.
So in comes El Neto.
El Neto, now has IGMP.
Has everything either switch and six has plus ICMP,
IGMP and RMI.
And we can now, oh, and now this is the interface.
El Neto uses.
As you can tell, like it's very,
it's based off of go primitive types.
Very simple, very easy to implement a stack or a node in El Neto.
And it's very maintainable.
And this right here, this, I believe, is the forever stack.
El Neto was here to stay.
It's a maintainer's dream come true.
El Neto gives me peace of mind that we'll be able to continue
development many years in the future without having to rewrite again.
It's, and it's so maintainable, like you wouldn't even believe.
We didn't, and we didn't lose a valuable feature.
We don't need to, like, wait for a packet to send packet.
And El Neto would not be possible without its less glamorous cousin
that device driver.
We got three networking drivers that work with El Neto, currently.
And we support most of the bells and whistles.
Not on all platforms.
But 10 megabit internet, 100 megabit internet, you're good.
And gigabit internet, soon to come.
So, maybe you'd guess.
There's lots of lessons to be learned from building three networking stacks.
I know I learned a couple at least.
The lessons we'll show you now.
I wish to show you, if you so let me.
Or that are co-ocoded and she coded.
So, green to black diamonds.
The green circle is, hey, this will probably be useful for you.
Just any developer out there, and the black diamond is, hey.
This is a very niche use case situation pattern.
First up, who cares about heap allocations here?
Show of hands?
Okay.
I was expecting much less hands.
I'd be suspicious of these people, right here you say.
Truth is, if you're using go in the, like, just normal go,
there's not much reason to care about heap allocations.
The go GC is amazing.
As you might have learned in Hesus's earlier talk.
And it's constantly evolving and improving.
On the other hand, I'd say I'd need to care.
That's what can process hundreds of packets of second
on these microcontrollers with very limited memory.
And the tiny go GC is not the same as a go GC.
And it has some rough edges.
So, if I just allocate a single byte on every packet,
eventually this program can event, like,
crash and take down my user's entire computer,
not just the program.
So, I need to care.
Maybe I can refresh this because making it a heapless means
removing all heap allocations.
Let's refresh, so it's something we can all think home.
I call it mechanical sympathy regarding heap allocations.
So, why have I listed this?
Well, it just requires a little bit of training.
I think that with knowing 20% of the places, the go GC allocates,
you can eliminate 80% of heap allocations.
And I think there's, um,
and there's a good example I got here for you,
to be risking your libraries.
Right here, I don't know, you probably can't see this,
but I've designed an interface for a decoder
in, for a MQTT protocol.
MQTT is like HTTP.
So, if you're not familiar with the MQTT,
think of HTTP.
And the MQTT protocol has strings that are processed
and then given to the user.
And these strings are unbounded.
They can be very large in size.
And I purposefully designed some, like,
this decoder interface that heap allocates these strings
to then give them to the user.
And my implementation I provide in the library is memory limited.
So, it's not going to allocate an arbitrary-sized string,
an arbitrary number of strings.
It will fill its buffer up,
and then just error out.
So, this gives you peace of mind being a library developer.
You don't have to worry about making your implementation
super-secure, um,
performance in the face of gigabyte-sized MQTT messages.
You can just provide the user with, like,
here's a, here's an interface.
If you care about gigabyte-sized messages,
implement the interface for your use case.
I'm going to provide a constrained size,
very simple implementation.
And my library, otherwise,
does no heap allocations.
So, you avoid CDs, basically,
by using this pattern.
Work smarts, not hard.
Next up, avoid global-meatable states.
Outside of package main, but you didn't hear that.
The go-standard library embraces global-meatable state in several cases.
The math-brand is part of most famous.
You're aware of, you know,
math-brand V2.
They're kind of regretting that decision.
Just don't do it.
The fix is that simple.
Identify your day of structures.
And add a method or kind of package that as an argument
to your functions.
Here's an example.
The second most used library in the go-eco system,
Google UUID.
It uses a global variable when you call new random,
called the render.
Just extract it.
Make a generator type.
The user can provide the random source with it.
And you can add all the config you want to this generator.
And it also simplifies your outward-facing API.
Now, you don't have new random before,
new random V5, new random with hash.
You can just put that all into the configuration aspect.
So, to me, this feels like a no-brainer improvement.
You're welcome, Google.
If you're here.
And for this one, let's jump straight into the example,
Composing Strucks with non-pointed types.
All you need to know is that this is like a moushka.
This is a code taken out of Lnetto.
You have like the sense-based, the control block, the handler,
the con, the TCP pool.
It's all like TCP's day-to-day structures.
And they're all like moushka into each other.
And tell me, does anyone see a pointer in there?
Maybe I'm hiding one behind value?
Well, the truth is, oh, yeah, there's a slice in there.
But no, you're too smart for your own good.
Yes, there is a slice.
Is there any other slice?
Oh, yeah.
But data structures, like Strucks.
I mean, avoid a pointer if you can at all.
This doesn't work when you have types that share internal data.
Like TCP pool is something I actually create an instance
as with a new constructor.
We can avoid using a new constructor, try using like reset
or configure methods.
And declaring your struct with a bar.
The bar keyword, name of your variable, Strucks type.
I found this to simplify the API a lot.
And you also avoid nil pointers, which is like something
people throw love, do do at go for.
Because nil pointer dereferences are like a risk in go.
And also, I'm not sure if that second example is always
viable, like valid.
If someone's familiar with go and can tell me, if I can assure that I
call, if I'm locked, when enter reset.
If that's valid, I'd be welcome.
I think it's valid, but I might not.
Take, just make, throw a check on that second example.
All right.
This one is very, there's a simple one.
Build code around primitive interfaces.
It's also very hard to explain.
There's a lot of hand waving.
I feel it's sort of an art.
All I can say is make your interfaces as simple as possible
and no simpler.
Sounds sort of pithy and meaningless.
But I feel like unless you're really, really good.
And I mean top go for it.
You won't get your interfaces right this first time.
Unless you're very familiar with the problem you're solving.
I've recently started the image processing library.
So the developing it.
And I'm sure I got the interface wrong.
Absolutely.
And I'll get it wrong a second time.
And if you got suggestions, if you're familiar with the image processing
at a low level, I'd welcome suggestions on this interface I'm designing.
Make this zero value useful.
And this is a Rob Pike quote.
This one you should be familiar with because it's one of the go-proverbs.
And why am I mentioning it at Boston?
So if it's something we all have heard before.
We've all seen that 2017, 15-ish talk.
Well, I feel it's not heated enough from my experience.
And I think there's also like a little.
I don't see it being used that often in enums as I use it.
Here's examples of how I use it in the networking stack.
The first case is state TCP closed.
The RFC for TCP defines the TCP program.
It defines that the closed state is an invalid state.
It's pre-initialization.
So it's a perfect case to have as a zero value for your enum of states.
The HTTP on the other end does not have a state for the pre-initialization.
For when your DHCP client is like an invalid state.
So I use the empty identifier.
I discard that name space.
I don't have a namespace for the DHCP state.
And I recognize it as being an invalid state.
Finally, when we configure Wi-Fi on this guy, which I just showed you.
And just put the test.
I define the undefined join-off enum value.
And when the user does not set it, set it in the config struct.
I know that it's undefined.
And I can make decisions based on other parameters in the config struct.
So if the user set a password, I'm pretty sure they're going to be
1ing WPA2 security.
If they have no password sets, pretty sure they're joining an open network.
Like the ones we have here at Faustum.
Synchronous API, concurrent package main.
Here's one I strongly suggest you follow.
Just make your API synchronous and create go routines in your package main.
Just avoid these three keywords in your third-party package is my suggestion.
Of course, there are use cases where maybe you're designing machine learning library.
And you want to parallelize certain processes.
And there's a very clear cut use case for it, then go for it.
Overall, if you're doing low-level stuff, I do low-level stuff in tiny go.
I always avoid these keywords in my third-party packages.
And then use them in package main.
And test-driven development.
This might get some drones.
TDD is like a contentious topic.
Note the qualifier, well-known problems.
Now working stacks, lectures, parsers, other well-study topics are well-suited
for TDD.
When you're input and output are known exactly down to the byte,
TDD has been the most effective way for me to develop.
Especially now, we have these AI agents that can generate these tests automatically for you.
So, that's up from that one.
Lastly, internal logging packages.
This sounds like a specific pattern I feel could be generalized.
I've used it for logging packages exclusively.
So it consists of making the API the logger used for your project internal.
Basically, hey, I do accept a slog logger from the user, which is like a standard library.
But then I use an internal package for logging in my whole third-party package.
And I'm going to skip ahead now.
It lets you do stuff like, hey, I may not want to compile my project.
Without the logging entirely, easy to do that.
And also add special logger to your logger, like,
printing all the heap allocations every time you see a diff in heap allocations.
That's use case I found tremendously useful.
And this, my friends, is a story for another Boston.
Well, what do you all think?
I mean, it feels like a bit too simple to be systems programming.
Maybe you're expecting a guide of how to implement lifetimes and go.
That'd be pretty sweet, but we won't solve most of my problems in El Nato.
Or maybe you'd be hoping that the final screen would be a prompt.
The prompt I used to vibe code El Nato.
Quality is pretty good, but not quite there yet.
And the answer is unsatisfyingly simple.
Go is a systems programming language.
You can write the drivers for the network interface.
And then you can write the networking stack that uses the drivers to send the packets.
And finally, the operating system, which handled the networking stacks.
And if you follow these guidelines, I think it will be better off.
That's all.
Thank you.
And thank you for my cousin for letting me stay in her apartment this week.
Thank you for the free rents.
Thank you.