WEBVTT

NOTE
This file was generated by Descript <www.descript.com>

00:00:13.732 --> 00:00:15.592
<v Amanda Majorowicz>This
is Self-Directed Research.

00:00:15.652 --> 00:00:19.522
Our hosts, James and Amos, get hyped about
different topics and take turns each week

00:00:19.522 --> 00:00:21.472
presenting their ideas to each other.

00:00:21.738 --> 00:00:24.138
You can check out the website,
YouTube or Spotify to watch

00:00:24.138 --> 00:00:28.698
this episodes presentation and
visit sdr-podcast.com/episodes

00:00:28.698 --> 00:00:31.878
for presentations, videos,
show notes and transcripts.

00:00:32.118 --> 00:00:34.038
New episodes are
published every Wednesday.

00:00:34.405 --> 00:00:36.475
This episode is brought
to you by CodeCrafters.

00:00:36.715 --> 00:00:39.835
Check out the link in our show notes or
listen to the end for more information.

00:00:39.985 --> 00:00:44.355
But first James brings you 75
slides about "A different serde".

00:00:50.840 --> 00:00:52.100
James, what do you have for us today?

00:00:52.307 --> 00:00:53.557
<v James Munns>Today's a different serde.

00:00:54.132 --> 00:00:57.762
We've talked a lot about serde things, and
now it's time to look a little deeper than

00:00:57.762 --> 00:01:01.782
Postcard and, uh, look at serde itself.

00:01:01.905 --> 00:01:05.385
A surprising number of people that I
talk to don't realize that serde comes

00:01:05.385 --> 00:01:08.471
from the name serialize and deserialize.

00:01:08.496 --> 00:01:11.976
And also everyone says it different,
I've heard 'sayrd' I've heard

00:01:12.186 --> 00:01:13.746
'sir-dee', I say 'sair-day'.

00:01:13.846 --> 00:01:16.781
But that's one of those things that
I'm sure people will let us know in

00:01:16.781 --> 00:01:18.671
the comment that they have opinions.

00:01:19.052 --> 00:01:21.952
<v Amos Wenger>I say 'saird' because
I'm French and also because that way

00:01:21.979 --> 00:01:26.999
my joke serde alternative works, but
I don't know if I'm allowed to bring

00:01:26.999 --> 00:01:28.309
it up because it's your episode James.

00:01:28.593 --> 00:01:31.613
<v James Munns>Serde as a quick recap,
to understand why I'm proposing

00:01:31.613 --> 00:01:33.843
something different, it's good to
understand what I'm talking about.

00:01:33.843 --> 00:01:35.613
So serde has two main parts.

00:01:35.893 --> 00:01:39.563
It's got a front end that thinks
about Rust types, so it's firmly in

00:01:39.563 --> 00:01:41.303
the world of Rust data structures.

00:01:42.123 --> 00:01:44.423
And it's got a back end that
thinks about the wire format.

00:01:44.423 --> 00:01:48.253
This is your JSON, your TOML, your
whatever collection of bytes you'd

00:01:48.253 --> 00:01:51.743
like to represent your stuff when
you write it to a file, or send it

00:01:51.743 --> 00:01:53.733
over the wire, or anything like that.

00:01:54.373 --> 00:01:57.429
And everyone uses serde, that's a really
good thing, because serde is much better

00:01:57.429 --> 00:02:02.139
than the alternative of casting your
structs to arrays of bytes, and then

00:02:02.139 --> 00:02:06.339
memcopying those to the wire, which is a
serious thing that unserious people do.

00:02:06.629 --> 00:02:07.609
Postcard uses serde!

00:02:07.639 --> 00:02:11.409
So postcard's built on top of serde,
because it has this flexibility between

00:02:11.409 --> 00:02:15.479
the front end and the back end, I can
define a very compact binary format,

00:02:15.699 --> 00:02:18.999
and serde's written in a very good
way that works with embedded systems,

00:02:19.009 --> 00:02:22.419
so even little microcontrollers can
use serde, the same way that big

00:02:22.419 --> 00:02:24.439
servers use serde, so it's wonderful.

00:02:24.849 --> 00:02:27.453
Despite the whole thing that I'm
going to say in this presentation,

00:02:27.453 --> 00:02:28.623
you should keep using serde.

00:02:28.623 --> 00:02:30.813
Serde is a very good tool that I like.

00:02:30.813 --> 00:02:33.393
Despite having messed around with
other things today, you should

00:02:33.393 --> 00:02:34.853
absolutely keep using serde...

00:02:36.093 --> 00:02:36.473
But...

00:02:37.193 --> 00:02:40.133
serde has some problems, and we're
going to talk about those a little bit.

00:02:40.653 --> 00:02:40.923
<v Amos Wenger>Oh boy.

00:02:41.178 --> 00:02:43.698
<v James Munns>Because I am
who I am, sometimes I wonder

00:02:43.698 --> 00:02:45.178
whether I could do it better.

00:02:45.208 --> 00:02:49.398
Maybe not for the general case, but
at least for what postcard needs.

00:02:49.458 --> 00:02:53.824
Because postcard is a very small,
simple format, and it doesn't need

00:02:53.824 --> 00:02:58.305
necessarily all the power of what every
single possible format could need.

00:02:58.485 --> 00:03:02.390
Like, uh, serde has to support for all
the formats that it has to support.

00:03:02.704 --> 00:03:04.674
<v Amos Wenger>Okay, I'm gonna make
this harder on you by interrupting

00:03:04.674 --> 00:03:05.954
you because it's my job as a co host.

00:03:05.984 --> 00:03:10.007
You said "Because I am who I am," and
I recently had a DSM party at my house.

00:03:10.057 --> 00:03:11.240
I don't know if you know what it is.

00:03:11.270 --> 00:03:14.627
It's where you read the DSM
III, IV and 5, the diagnosis

00:03:14.627 --> 00:03:16.087
manual for mental disorders...

00:03:17.428 --> 00:03:20.847
It's very interesting to see
that you only have a    thing

00:03:21.277 --> 00:03:22.604
if it's a problem for others.

00:03:22.904 --> 00:03:26.254
So as long as it only bothers you,
you're not clinically anything.

00:03:27.574 --> 00:03:27.734
<v James Munns>Hehehehehehe.

00:03:28.064 --> 00:03:28.664
Yeah.

00:03:28.664 --> 00:03:33.658
there's some sleep phase, delayed sleep
phase disorder or something like that

00:03:33.658 --> 00:03:36.648
and it was weird to read Wikipedia and
I go, "They're describing my life."

00:03:37.088 --> 00:03:39.168
But yeah, I think I kinda
know what you're getting at.

00:03:39.882 --> 00:03:43.182
But, how serde works: so I mentioned
that it's got a front end and a back end.

00:03:43.712 --> 00:03:47.772
Serde has a data model, so it's
got essentially a limited palette

00:03:47.812 --> 00:03:49.432
of types that it thinks in.

00:03:49.492 --> 00:03:52.182
The palette's not so limited,
there's 29 different types.

00:03:52.212 --> 00:03:55.942
This covers everything from
primitive integer types, or

00:03:55.952 --> 00:03:57.892
primitive other numeric types,

00:03:58.222 --> 00:04:03.492
arrays like string slices, arrays
of T, byte slices, things like that,

00:04:03.962 --> 00:04:08.592
composite types like structs and tuple, so
a type that's made up of other types and

00:04:08.592 --> 00:04:12.462
discriminated types like enum and
their variants and all the different

00:04:12.462 --> 00:04:14.572
flavors of enums that you can have.

00:04:14.922 --> 00:04:19.530
So these all   collectively make up
the 29 data types that serde thinks in.

00:04:19.964 --> 00:04:21.014
I mentioned that there's two parts.

00:04:21.014 --> 00:04:25.794
The front end's job is to turn Rust
types into data model types, or for

00:04:25.794 --> 00:04:29.524
deserialization the other way around,
data model types into Rust types.

00:04:30.299 --> 00:04:32.709
And the backend takes those
array of strings and turns

00:04:32.709 --> 00:04:33.869
them into bytes on the wire.

00:04:33.879 --> 00:04:36.039
For JSON, that's text.

00:04:36.049 --> 00:04:41.774
For postcard, that's a length prefix and
then just the raw UTF-8 bytes on the wire.

00:04:41.984 --> 00:04:43.174
But that's the two step process.

00:04:43.174 --> 00:04:46.974
We kind of have that even middle
meeting point from whatever type you

00:04:46.974 --> 00:04:48.824
want to whatever format you want.

00:04:49.074 --> 00:04:53.464
There's sort of always that middle step
of picking which data model type you're

00:04:53.464 --> 00:04:55.314
choosing to represent that data as.

00:04:56.022 --> 00:04:59.442
The other way that serde works is by
using what's called the visitor pattern.

00:04:59.512 --> 00:05:04.042
Because there's sort of this two moving
part thing, you kind of have to figure

00:05:04.042 --> 00:05:07.272
out: well, if I, if I was making it
totally generic, I'd have to be generic

00:05:07.272 --> 00:05:09.872
over the type and the serializer or what?

00:05:09.922 --> 00:05:14.097
And serde chooses a programming pattern
that's popular in other languages.

00:05:14.097 --> 00:05:16.537
I know I've seen it in
C++ and Java, I think.

00:05:16.797 --> 00:05:20.456
But the visitor pattern, which is
essentially you hand the type the

00:05:20.456 --> 00:05:23.496
serializer and it drives that pattern.

00:05:23.936 --> 00:05:25.746
These types are given a serializer,

00:05:26.146 --> 00:05:29.456
and each of those data model
types have a serializing method.

00:05:29.566 --> 00:05:30.926
So the type can say:

00:05:31.146 --> 00:05:34.376
okay, I've got this field that's an
integer- or let's say specifically a

00:05:34.376 --> 00:05:38.506
u8- I'm going to call `.serialize_u8()`
on it, and I'm going to give it

00:05:38.506 --> 00:05:42.876
the u8, and then it will turn
that into the bytes on the wire.

00:05:43.226 --> 00:05:49.806
And so the type is the one that drives
the visitor, and then the data format is

00:05:49.806 --> 00:05:54.506
the one that says: this is how I turn a u8
into whatever representation makes sense

00:05:54.506 --> 00:05:56.237
in JSON or TOML or anything like that.

00:05:56.667 --> 00:06:00.077
So you would call your serialize
u8, serialize str, and you're

00:06:00.077 --> 00:06:05.322
handing it the instance of that
type that serde natively thinks in.

00:06:06.522 --> 00:06:07.552
So this drives the backend.

00:06:07.552 --> 00:06:10.362
Postcard says, "Ah, I'm
being told to serialize a u8.

00:06:10.362 --> 00:06:11.312
I will put that on the wire.

00:06:11.312 --> 00:06:13.562
I'm being told to serialize a str.

00:06:13.612 --> 00:06:14.822
I'm going to put that on the wire."

00:06:14.822 --> 00:06:18.142
It doesn't really think about
the context of what's going on.

00:06:18.142 --> 00:06:20.212
It just says: I've been
told to do this, "bleh".

00:06:20.507 --> 00:06:23.557
Data goes out on the wire or:
I've been told to retrieve

00:06:23.557 --> 00:06:25.257
a u8 or a str from the wire.

00:06:25.467 --> 00:06:30.457
I will try and consume up some bytes and
turn it into a str or a u8 or whatever.

00:06:31.937 --> 00:06:35.476
Now this visitor driver
code is usually derived.

00:06:35.486 --> 00:06:38.734
You usually don't have to
write this even for other types

00:06:38.734 --> 00:06:43.414
You just throw a derive serialize on
there and a procedural macro goes in and

00:06:43.414 --> 00:06:45.984
generates: okay, I have seven fields.

00:06:46.024 --> 00:06:47.264
The first field is this.

00:06:47.514 --> 00:06:48.974
Do this, do this, do this, do this.

00:06:48.974 --> 00:06:53.234
And it expands out into essentially
driving that visitor at all points.

00:06:54.034 --> 00:06:59.044
So you can take a type like this, a
struct that has two fields, put derive

00:06:59.044 --> 00:07:04.782
serialize on it, and you get a relatively
lot more code, which runs this process.

00:07:05.042 --> 00:07:07.362
We're implementing
serialized for PWM error.

00:07:07.762 --> 00:07:11.362
It starts a struct, it does each field
of the struct, and then it ends a struct.

00:07:11.552 --> 00:07:14.382
Postcard doesn't care about the
start and the end, but if you were in

00:07:14.382 --> 00:07:17.562
JSON, you might want to like indent
and start a new curly brace, and

00:07:17.562 --> 00:07:21.422
then do the fields But it's got the
ability to handle all of those cases.

00:07:22.317 --> 00:07:26.421
<v Amos Wenger>I, um, cannot help
but notice on this slide, the third

00:07:26.431 --> 00:07:31.131
argument to serialize structs is
false as usize plus one plus one?

00:07:32.270 --> 00:07:36.380
I'm sure there's a reason for it, but it's
a proc macro, why can't it just say two?

00:07:36.534 --> 00:07:37.564
<v James Munns>I have no idea.

00:07:37.847 --> 00:07:41.748
It's probably one of those, like,
" Who am I to question dtolnay when

00:07:41.858 --> 00:07:43.488
comes to writing proc macros?"

00:07:43.508 --> 00:07:46.138
These are arcane that I
do not want to know about.

00:07:46.140 --> 00:07:47.583
<v Amos Wenger>I will look into this.

00:07:47.632 --> 00:07:48.832
I want to look into this!

00:07:48.832 --> 00:07:49.072
<v James Munns>Yeah.

00:07:49.072 --> 00:07:51.468
The other thing is serde also, I
think, supports all the way back

00:07:51.468 --> 00:07:53.818
to like Rust 2015 or whatever.

00:07:53.818 --> 00:07:57.398
So sometimes it does some weird
anachronistic things because

00:07:57.408 --> 00:08:00.828
it has to work for all versions
of Rust and things like that.

00:08:01.058 --> 00:08:04.878
<v Amos Wenger>Granted, but I'm pretty sure
we've had integer literals for a while.

00:08:04.888 --> 00:08:07.538
So like zero has been there
the whole time that I'm pretty

00:08:07.538 --> 00:08:08.958
curious what it's there for.

00:08:09.004 --> 00:08:10.746
<v James Munns>That's a whole
other episode, I guess.

00:08:11.296 --> 00:08:15.216
And there are some problems with
deriving all of that code other than

00:08:15.216 --> 00:08:18.055
just false as a integer for some reason.

00:08:18.645 --> 00:08:21.655
So the main problem is that
it generates a lot of code.

00:08:21.885 --> 00:08:27.108
It can generate a sometimes
double digit multiple of the input

00:08:27.108 --> 00:08:28.618
source that you are giving it.

00:08:28.868 --> 00:08:30.667
This is a picture of a Bluesky post.

00:08:30.690 --> 00:08:33.763
This is from a customer project
where  it's a little bit silly and

00:08:33.763 --> 00:08:35.158
I think they've fixed it since then.

00:08:35.188 --> 00:08:39.272
But they have 50,000 lines of code
in one crate of raw source code,

00:08:39.272 --> 00:08:43.182
like if you just run tokei on the
project, you get about 50,000 lines.

00:08:43.542 --> 00:08:46.072
If you do the expansion of that,
so you run it through cargo-expand

00:08:46.582 --> 00:08:50.462
and then count the expanded lines,
it's over 200,000 lines of code.

00:08:50.492 --> 00:08:54.162
And one of the file that is basically
just type definitions was a little

00:08:54.162 --> 00:08:57.072
over a thousand lines of code, and
after expansion of just that one

00:08:57.072 --> 00:08:59.609
file, became 22,000 lines of code.

00:08:59.659 --> 00:09:02.789
There's a lot going on there, but
a lot of code is a lot of code.

00:09:03.454 --> 00:09:06.054
<v Amos Wenger>Yeah, and the thing is, it's
all generic code as well, so it's going

00:09:06.054 --> 00:09:10.174
to be instantiated using  a serializer
or a deserializer implementation.

00:09:10.254 --> 00:09:13.421
So that's just template
in C++ parlance, I guess.

00:09:13.524 --> 00:09:13.774
<v James Munns>Yeah.

00:09:13.843 --> 00:09:15.793
And this has measurable impact.

00:09:15.803 --> 00:09:19.033
I mean, the reason I was looking at
this is because it took 40 seconds

00:09:19.033 --> 00:09:22.723
to do a build of that one crate and
this was their main binary crate.

00:09:22.723 --> 00:09:25.563
So every time they touched it,
it would take like 40 seconds

00:09:25.563 --> 00:09:28.053
to rebuild, which was painful.

00:09:28.986 --> 00:09:32.406
And like you were saying, there's a
lot of monomorphization and inlining.

00:09:32.406 --> 00:09:37.583
This isn't necessarily serde's fault,
but Rust, like you said: generics

00:09:37.583 --> 00:09:40.433
are templates, basically, so it
will stamp out a version of this.

00:09:40.683 --> 00:09:43.903
If you have multiple serializers,
it will stamp out a version of

00:09:43.953 --> 00:09:45.003
each of these that are used.

00:09:45.483 --> 00:09:49.273
And when it comes to the optimizer,
we will inline a lot of this, which

00:09:49.273 --> 00:09:54.413
means some of those instantiated
copies will end up being one very large

00:09:54.463 --> 00:09:57.683
function, and then you'll have multiple
versions of that function because

00:09:57.683 --> 00:09:59.183
they get inlined in different places.

00:09:59.463 --> 00:10:03.805
And so it can end up taking a fair
amount of compilation and optimization

00:10:03.815 --> 00:10:07.251
time, but then end up as a lot
of code in your binary as well.

00:10:07.531 --> 00:10:10.068
<v Amos Wenger>Yeah, that's what I was
thinking when you were presenting

00:10:10.097 --> 00:10:13.603
the visitor pattern and how we have
separate methods for separate types.

00:10:13.822 --> 00:10:17.177
I was thinking, because in my serde
alternative-  which it's not the

00:10:17.177 --> 00:10:21.015
topic of today's presentation, believe
it or not- I just have an enum.

00:10:21.203 --> 00:10:23.243
But then if you have an enum,
you have dynamic dispatch.

00:10:23.243 --> 00:10:26.593
You have to match on the discriminant of
the enum and then know if it's a string,

00:10:26.593 --> 00:10:27.823
if it's a number, if it's whatever.

00:10:27.823 --> 00:10:29.933
Also, it's inefficient for a bunch
of other reasons, but the reason

00:10:29.933 --> 00:10:33.743
serde has methods per type is
that it can inline everything.

00:10:34.043 --> 00:10:35.843
There's no dynamic dispatch at all.

00:10:36.153 --> 00:10:38.373
It all gets compiled down to
just compact code, and you

00:10:38.373 --> 00:10:39.953
pay for that in compile times.

00:10:40.053 --> 00:10:42.703
And I think a lot of the times when
people complain about Rust compile

00:10:42.703 --> 00:10:46.063
times, they're actually complaining
about serde generic instantiations.

00:10:46.382 --> 00:10:47.752
<v James Munns>It's one of those
things: it's a useful tool.

00:10:47.802 --> 00:10:51.002
And that's one of those hard
problems of in isolation of just a

00:10:51.002 --> 00:10:52.182
couple of types, it would be fine.

00:10:52.182 --> 00:10:55.192
But that customer project that I was
talking about, they have hundreds of types

00:10:55.222 --> 00:10:59.922
that each have many dependencies that all
generate code and to be fair, on one hand

00:10:59.922 --> 00:11:04.992
it's a problem of success of it has gotten
so successful and it's so widely used.

00:11:05.002 --> 00:11:06.702
It becomes the easiest thing to point at.

00:11:06.762 --> 00:11:09.522
I don't want to point too hard
because it is a good tool, but

00:11:09.522 --> 00:11:11.435
it gets creaky at a certain size.

00:11:11.515 --> 00:11:13.895
<v Amos Wenger>Of course, now that I
think about all this: if you were to,

00:11:13.905 --> 00:11:18.265
like, take types and put a hundred
in different crates and then export

00:11:18.285 --> 00:11:21.915
non generic functions, then you
could actually cache some of it.

00:11:22.095 --> 00:11:26.215
The problem is that you have all the types
in one crate, in 50,000 lines of code.

00:11:26.348 --> 00:11:29.817
I'm just, realizing that I've been working
for six months on an alternative when

00:11:29.817 --> 00:11:32.901
actually there was a compiler hack that
could have done everyth- Don't mind me.

00:11:33.271 --> 00:11:33.851
I'll be over there.

00:11:34.211 --> 00:11:34.531
<v James Munns>Okay.

00:11:34.581 --> 00:11:37.303
Another problem is that the
visitor pattern is recursive.

00:11:37.533 --> 00:11:42.284
When you call serialize on a field that
is being serialized, you're then calling

00:11:42.294 --> 00:11:45.944
that serialize method, which then if
it has children, calls those, which

00:11:45.944 --> 00:11:50.464
calls those, which calls those, which
is not always necessarily a problem.

00:11:50.464 --> 00:11:55.099
It's a nice way of writing the code
because every struct is essentially

00:11:55.119 --> 00:11:59.369
isolated to its world, and when you
have to go into another world, like for

00:11:59.509 --> 00:12:02.809
serializing one of your children, you
just call a single serialize method of it.

00:12:02.819 --> 00:12:08.022
So it makes it locally very simple, but
when you have very deeply nested types or

00:12:08.377 --> 00:12:12.577
things like that, the call chain can get
somewhat deep on this and especially the

00:12:12.577 --> 00:12:17.407
more functions you call, you're eating
up stack frames and usually returning

00:12:17.407 --> 00:12:22.135
things back down those stack frames,
which can add up to a little bit of cost.

00:12:22.440 --> 00:12:25.110
<v Amos Wenger>Did you actually run into
that in embedded or did you run into

00:12:25.110 --> 00:12:27.910
the opposite problem where like, the
structure is not that deep but the

00:12:27.910 --> 00:12:31.636
stack that you have is smaller than
you're used to on desktop, for example?

00:12:31.742 --> 00:12:32.542
<v James Munns>It can.

00:12:32.542 --> 00:12:35.612
Especially for deserialization,
data is returned by value.

00:12:35.642 --> 00:12:39.876
And sometimes because we don't have a
heap vector, we will use stack allocated

00:12:39.876 --> 00:12:43.516
vectors where you say: okay, here's a
bounded vector with room for 32 items.

00:12:43.734 --> 00:12:47.943
And when you return that deserialization
step by value, even if you only

00:12:47.963 --> 00:12:51.853
deserialized one item, if it's a fixed
size collection, essentially you have

00:12:51.853 --> 00:12:56.333
the capacity for all 32, you're going to
return that by value down the stack frame.

00:12:56.563 --> 00:13:01.893
Which means you've created that whole
32 item vec in one function, and then

00:13:01.893 --> 00:13:06.395
returning it by value means you memcpy
it back to the caller frame, essentially.

00:13:06.395 --> 00:13:10.925
And this is one of those things that
LLVM can optimize sometimes, but RVO

00:13:10.925 --> 00:13:13.345
is not a guaranteed optimization.

00:13:13.345 --> 00:13:15.720
<v Amos Wenger>RVO is
Return Value Optimization?

00:13:16.040 --> 00:13:18.500
<v James Munns>Yeah, so there's RVO,
which is Return Value Optimization,

00:13:18.500 --> 00:13:22.930
and NRVO, which is Named Return Value
Optimization, and the idea is that

00:13:22.930 --> 00:13:29.291
essentially you make space in the
caller, and use that in the callee, so

00:13:29.291 --> 00:13:33.361
that when it's populating that, it's
actually populating it to the destination.

00:13:33.631 --> 00:13:36.911
And when you return, you actually
end up just returning nothing, and

00:13:36.961 --> 00:13:40.241
the data is already there, is the
transform you're kinda going for there.

00:13:40.501 --> 00:13:44.010
<v Amos Wenger>This might be where
you're driving, but as far as I know,

00:13:44.010 --> 00:13:48.830
serde has a deserialized in place,
like a lot of Rust APIs include an in

00:13:48.830 --> 00:13:52.837
place version because of the lack of
guaranteed RVO and NRVO, is that...?

00:13:53.048 --> 00:13:53.998
<v James Munns>I haven't run into it.

00:13:54.262 --> 00:13:55.996
<v Amos Wenger>It's
doc(hidden), is the fun thing.

00:13:56.956 --> 00:14:00.436
So I only found out about it because I've
been snooping around the source code.

00:14:00.576 --> 00:14:01.406
<v James Munns>I haven't run into that.

00:14:01.476 --> 00:14:02.506
I might have to poke into that.

00:14:02.606 --> 00:14:03.306
<v Amos Wenger>Yeah, you should.

00:14:03.349 --> 00:14:05.459
<v James Munns>And for postcard it
has more flexibility than we need.

00:14:05.509 --> 00:14:09.009
The visitor drivers here have
the capability: so, for example,

00:14:09.009 --> 00:14:11.895
in JSON, the fields don't always
have to be in the same order.

00:14:11.945 --> 00:14:13.345
You might order them differently.

00:14:13.345 --> 00:14:16.605
Like JavaScript might order the fields
in one way and Rust orders it in another

00:14:16.605 --> 00:14:20.625
way, which means for certain types, you
have to be able to visit each things

00:14:20.635 --> 00:14:24.205
out of order, fill them in and still
end up with the ones that you need.

00:14:24.585 --> 00:14:26.185
Postcard does not do that.

00:14:26.235 --> 00:14:29.095
It says they are always
in the lexical order.

00:14:29.375 --> 00:14:31.865
Tuples are always, you
know, zero to whatever.

00:14:31.865 --> 00:14:33.245
Structs are always top to bottom.

00:14:33.369 --> 00:14:38.059
And then when you get into attributes
like rename or skip serializing if or

00:14:38.059 --> 00:14:43.365
skip deserializing if, there's hooks
at certain point where the visitor can

00:14:43.375 --> 00:14:45.785
change behavior or even flatten things.

00:14:45.832 --> 00:14:49.387
Which one makes postcard very upset
because it doesn't like that And

00:14:49.407 --> 00:14:53.077
two it's just flexibility that
you don't necessarily need in all

00:14:53.077 --> 00:14:56.607
cases I'm sure there are people who
love those functionalities, but at

00:14:56.607 --> 00:14:58.097
least for me and for postcard...

00:14:58.433 --> 00:14:59.187
I don't want it.

00:14:59.283 --> 00:15:01.963
<v Amos Wenger>Wait, so are you paying
for code that is able to handle

00:15:01.963 --> 00:15:05.093
fields out of order even though
that never happens with postcard?

00:15:05.166 --> 00:15:07.786
So you get visited with key value
pairs I would imagine and then you

00:15:07.786 --> 00:15:10.546
can do whatever you want and you
could in the implementation assume

00:15:10.546 --> 00:15:12.006
that they're in the expected order?

00:15:12.111 --> 00:15:12.571
I don't know.

00:15:12.836 --> 00:15:15.136
<v James Munns>I think it's one of
those things that if it doesn't exist,

00:15:15.136 --> 00:15:16.776
it's almost certainly optimized out.

00:15:17.616 --> 00:15:21.940
I believe that serde has the latitude
for that, but I don't necessarily

00:15:21.950 --> 00:15:24.730
think that we're paying extra for
that in postcard by not using it.

00:15:24.780 --> 00:15:27.940
There's certain attributes that do
trigger that to happen, and these are

00:15:27.940 --> 00:15:31.972
the ones that confuse postcard, because
postcard doesn't keep track of what you

00:15:31.972 --> 00:15:33.792
visited and not visited at each step.

00:15:33.893 --> 00:15:36.833
So if you try and do things out of
order, it just grabs wrong fields in

00:15:36.833 --> 00:15:38.049
the wrong order I'd have to check.

00:15:38.239 --> 00:15:38.949
The answer is I don't know.

00:15:39.269 --> 00:15:39.739
<v Amos Wenger>Got it.

00:15:39.896 --> 00:15:42.266
<v James Munns>This is a fairly
reasonable way to approach

00:15:42.266 --> 00:15:43.406
the problem of serialization.

00:15:43.416 --> 00:15:47.436
You break it down to each type, you
locally figure out how to handle each

00:15:47.436 --> 00:15:51.316
type, and then you handle it, and
then you do it sort of recursively.

00:15:51.456 --> 00:15:56.006
It's like a very straightforward, well
thought out way to approach this problem.

00:15:56.556 --> 00:15:59.658
So how do we even think about other
ways of approaching this problem?

00:16:00.258 --> 00:16:04.726
Well, uh, I spent some time looking into a
weird archaic programming language called

00:16:04.726 --> 00:16:06.726
forth and it broke my brain a little bit.

00:16:07.060 --> 00:16:10.010
Now I'm going to talk a little
bit about postcard-forth, which is

00:16:10.240 --> 00:16:13.450
an experiment I did a while back
that had some interesting results.

00:16:13.690 --> 00:16:16.260
It's not ready to use by
any sort of the imagination.

00:16:16.393 --> 00:16:20.863
I'm going to slap a big old experimental
sticker on that because it's- it's gross,

00:16:20.903 --> 00:16:23.073
but it's an interesting research project.

00:16:23.513 --> 00:16:26.966
How postcard-forth works
is a little different.

00:16:27.059 --> 00:16:29.008
We do keep mostly the same data model.

00:16:29.008 --> 00:16:32.868
We say, look, there's a limited palette
of types that you can convert to and from.

00:16:32.868 --> 00:16:36.518
That's sort of our bridge from
Rust world into the wire world.

00:16:36.518 --> 00:16:39.347
And postcard really already has
this because I only define the

00:16:39.347 --> 00:16:42.627
29 different things you can put
on the wire in postcard format.

00:16:42.867 --> 00:16:43.977
So we're kind of already there.

00:16:44.060 --> 00:16:45.750
Mostly keep the same data model.

00:16:46.047 --> 00:16:50.600
Skipping ahead a little bit: we would
have a much simpler derive macro.

00:16:50.830 --> 00:16:53.640
So rather than generating that
visitor pattern, which is going to

00:16:53.640 --> 00:16:57.790
generate functions for us that are
going to drive that serialization

00:16:57.790 --> 00:16:58.370
or deserialization process,

00:16:59.050 --> 00:17:04.380
we only generate a list of field
offsets and function pointers

00:17:04.390 --> 00:17:08.340
for functions that know how to
serialize or deserialize that data.

00:17:08.643 --> 00:17:11.103
So for a type that looks like this:

00:17:11.441 --> 00:17:14.681
outer that has three fields,
one of them being inner.

00:17:14.881 --> 00:17:18.271
A struct inner that has two fields,
some integers, some strings,

00:17:18.281 --> 00:17:19.661
some Vecs, things like that.

00:17:20.031 --> 00:17:22.711
Instead of something vaguely
like this from serde,

00:17:22.751 --> 00:17:28.091
where we'd have outer, which is
going to call str, then serialize

00:17:28.111 --> 00:17:30.631
on the inner struct, which is going
to call ser- you know what I mean.

00:17:30.847 --> 00:17:33.487
Instead of implementing
the trait like this,

00:17:33.671 --> 00:17:36.301
instead we generate
something sort of like this:

00:17:36.351 --> 00:17:38.331
which is a constant array.

00:17:38.531 --> 00:17:42.951
And each element of the array is a
tuple of a usize and a function pointer.

00:17:43.259 --> 00:17:47.837
The function pointer takes a
const pointer to the unit type.

00:17:47.847 --> 00:17:51.377
So basically a type erased
pointer and a mutable reference

00:17:51.377 --> 00:17:53.073
to an output stream of bytes.

00:17:53.643 --> 00:17:56.153
So what this is trying to say
is we have an array of things.

00:17:56.183 --> 00:18:00.993
We have the offset from the base of the
struct to this specific field, and then

00:18:00.993 --> 00:18:03.393
the function that's going to handle that.

00:18:03.615 --> 00:18:05.395
<v Amos Wenger>I have several comments.

00:18:05.405 --> 00:18:09.445
First: if you're listening to this,
every time James says this, he

00:18:09.445 --> 00:18:10.875
is actually pointing at a screen.

00:18:10.885 --> 00:18:12.515
You could be having this on your screen.

00:18:12.715 --> 00:18:17.930
You can find the slides at
sdr-podcast.com/episodes if you

00:18:17.930 --> 00:18:20.320
want to and then find the episodes
we're talking about and you can

00:18:20.330 --> 00:18:21.420
have the slide deck on there.

00:18:21.590 --> 00:18:25.306
Second: I think it's hilarious because I'm
looking at the slides, that James went the

00:18:25.306 --> 00:18:29.602
exact opposite direction that I went with
the slides where he's using a pixel font.

00:18:29.952 --> 00:18:33.053
And I went with a pretty- what
did I get- Atkinson Hyperlegible?

00:18:33.659 --> 00:18:36.140
So this is like polar
opposites typographic choices.

00:18:36.400 --> 00:18:41.142
Thirdly: James, isn't that ffi unsafe to
have a const pointer to the unit tuple?

00:18:41.783 --> 00:18:43.447
<v James Munns>What do you mean, ffi unsafe?

00:18:43.531 --> 00:18:45.521
<v Amos Wenger>It probably
doesn't matter actually, because

00:18:45.571 --> 00:18:47.024
this is staying inside Rust.

00:18:47.054 --> 00:18:48.134
This is a C pattern.

00:18:48.233 --> 00:18:51.019
In C, when you have an API to do
something, there's always a void pointer.

00:18:51.060 --> 00:18:51.693
<v James Munns>Yeah, this is void*.

00:18:52.113 --> 00:18:53.113
This is Rust void*.

00:18:53.443 --> 00:18:53.683
Yeah...

00:18:53.736 --> 00:18:56.807
<v Amos Wenger>Yeah, yeah, this is
void* pointer, context, whatever.

00:18:56.863 --> 00:18:58.367
you'll see that in all C style APIs.

00:18:58.367 --> 00:18:59.824
So seeing it here, brings me back.

00:18:59.824 --> 00:19:01.157
The trauma- it's all coming back to me.

00:19:01.247 --> 00:19:04.757
<v James Munns>Oh yeah, this is
explicitly type erased pointers to data.

00:19:04.787 --> 00:19:06.996
<v Amos Wenger>But it's not exactly the
same thing like I think if you put

00:19:06.996 --> 00:19:11.506
them in an extern c function, the rustc
will yell at you saying like, "It's not

00:19:11.516 --> 00:19:14.495
guaranteed that this is actually same
size as a void pointer or whatever..."

00:19:14.495 --> 00:19:17.724
<v James Munns>You can't pass a tuple
because C doesn't have a concept of

00:19:17.724 --> 00:19:20.304
zero size types and C++ doesn't either.

00:19:20.354 --> 00:19:23.747
But a pointer to a zero
size type is still allowed.

00:19:23.807 --> 00:19:28.693
That's how I believe opaque type
works in C is that it's a size that

00:19:28.693 --> 00:19:30.163
the compiler does not know about.

00:19:30.423 --> 00:19:33.783
And that's essentially how unsized
works in C because it doesn't

00:19:33.793 --> 00:19:37.428
have a proper concept of unsized,
but, uh, I could be wrong.

00:19:37.666 --> 00:19:40.436
I think you get the point that I'm
going for like void* pointer here.

00:19:40.487 --> 00:19:40.717
<v Amos Wenger>Sure.

00:19:40.717 --> 00:19:40.934
Sure.

00:19:40.934 --> 00:19:41.151
Sure.

00:19:41.151 --> 00:19:41.367
Sure.

00:19:41.367 --> 00:19:44.983
My other question was isn't offset off
something that was just stabilized?

00:19:45.078 --> 00:19:47.943
<v James Munns>Offset of
enums is stabilizing.

00:19:48.075 --> 00:19:52.108
Offset of has been there for a
while, but probably this year or

00:19:52.108 --> 00:19:53.378
last year or something like that.

00:19:53.412 --> 00:19:56.610
<v Amos Wenger>Something like
this was in a recent changelog.

00:19:56.780 --> 00:19:58.052
I forget which...

00:19:58.252 --> 00:20:00.245
<v James Munns>They're still working
on it because offset of enums

00:20:00.255 --> 00:20:02.960
is not totally stable yet, and
we'll get to that cause there's an

00:20:02.960 --> 00:20:04.540
RFC I'm going to mention, but...

00:20:04.726 --> 00:20:07.006
offset of itself is stable at this point.

00:20:07.056 --> 00:20:09.206
<v Amos Wenger>Also I'm looking
at the slide, which again, go to

00:20:09.266 --> 00:20:14.996
sdr-podcast.com/episodes to find
out: but does `ser_deref_slice_u8`

00:20:15.079 --> 00:20:18.942
turbofish vec u8, which gives you
the instantiated version of that, the

00:20:18.942 --> 00:20:22.180
monomorphized version of that function,
and that's a thin function pointer.

00:20:22.180 --> 00:20:23.270
<v James Munns>Yes, exactly.

00:20:23.270 --> 00:20:26.595
You can monomorphize generic
functions and not put the

00:20:26.595 --> 00:20:27.575
parentheses at the end of them.

00:20:27.575 --> 00:20:31.425
So the turbofish actually does the
instantiation of that specific version.

00:20:31.605 --> 00:20:34.285
So it is no longer a generic function.

00:20:34.391 --> 00:20:37.981
It is a specifically typed
function pointer at this point.

00:20:38.240 --> 00:20:42.820
You use this a lot in Waker dispatch
tables or what are they called...

00:20:42.910 --> 00:20:46.340
the vtable that you have to
create for async functions.

00:20:46.340 --> 00:20:48.105
<v Amos Wenger>Right, for
futures, yeah, yeah.

00:20:48.190 --> 00:20:50.796
This makes a terrifying amount
of sense, but I don't think I

00:20:50.796 --> 00:20:51.516
would have thought about it.

00:20:51.516 --> 00:20:53.184
You just blew my mind, James.

00:20:54.180 --> 00:20:56.430
<v James Munns>So we ended up
with this array, and even though

00:20:56.430 --> 00:20:58.650
we're still generating, you know,
the length isn't so different,

00:20:58.650 --> 00:20:59.940
especially for these simple ones.

00:21:00.233 --> 00:21:05.580
The neat part about this is the offset is
a usize, so it's one word, four or eight

00:21:05.590 --> 00:21:07.160
bytes depending on what platform you are.

00:21:07.540 --> 00:21:10.120
A thin function pointer is one word.

00:21:10.525 --> 00:21:17.101
So, for those two fields, that is
8, 16, 32 bytes for that, plus it's

00:21:17.101 --> 00:21:19.901
a slice, so maybe another one and
a pointer or something in there.

00:21:20.091 --> 00:21:23.731
Something on the order of a handful
of words, and same for outer.

00:21:23.911 --> 00:21:28.216
So, code is usually pretty dense,
but I'd probably bet that this is

00:21:28.216 --> 00:21:32.316
maybe a little shorter than even the
generated optimized code, particularly

00:21:32.336 --> 00:21:33.846
before you've inlined it and stuff.

00:21:33.866 --> 00:21:35.996
And we're just working with data.

00:21:36.016 --> 00:21:39.736
It is a constant value that doesn't
get used unless it gets used.

00:21:39.746 --> 00:21:42.896
It's not even code that has
been generated at this point.

00:21:43.596 --> 00:21:47.336
I've already talked about doing
tricky things with macros and const

00:21:47.346 --> 00:21:51.466
functions to merge arrays of things
so that you end up with one array.

00:21:51.466 --> 00:21:56.636
So maybe for that outer one, we don't
even have that call to the offsets of

00:21:56.636 --> 00:22:00.276
the parent fields and then calling into
some function that handles the inner.

00:22:00.586 --> 00:22:05.186
We just splat the entire list into
one list and all of a sudden we

00:22:05.186 --> 00:22:07.009
have the entire makeup of this.

00:22:07.019 --> 00:22:11.579
And even though these are repr(Rust)
types, offset of works with them.

00:22:11.589 --> 00:22:16.429
So you can take the base of one struct
and add the offset to that field

00:22:16.689 --> 00:22:21.214
and then add offsets to the subfield
and splat it and get an entirely

00:22:21.214 --> 00:22:24.004
correct offset of that subfield.

00:22:24.344 --> 00:22:25.894
<v Amos Wenger>James, token listener here.

00:22:25.994 --> 00:22:30.174
First of all: where- what- wha- which
context did you learn the word 'splat' in?

00:22:30.184 --> 00:22:31.114
Because I know what you mean.

00:22:31.144 --> 00:22:32.274
I'm thinking of Javascript.

00:22:32.324 --> 00:22:33.194
<v James Munns>That's a Python thing.

00:22:33.194 --> 00:22:33.264
Yeah.

00:22:33.264 --> 00:22:36.654
Splatting is when you had an array
of three things, if you were to splat

00:22:36.654 --> 00:22:39.964
it in Python, that would be then
passing the function three arguments

00:22:40.194 --> 00:22:42.644
instead of an array of three elements.

00:22:42.774 --> 00:22:45.543
<v Amos Wenger>Because this is a
presentation about Rust and serde, it's

00:22:45.543 --> 00:22:47.663
the equivalent of serde flatten, right?

00:22:48.380 --> 00:22:53.240
<v James Munns>Sort of, except for we're
not flattening the types necessarily.

00:22:53.240 --> 00:22:57.805
We've just merged the child list with
the parent list so that the parent

00:22:57.815 --> 00:22:59.744
list just- it's a flattening operation.

00:22:59.894 --> 00:23:02.224
Same kind of thing we are
flattening here for sure.

00:23:02.314 --> 00:23:02.604
<v Amos Wenger>Cool.

00:23:02.624 --> 00:23:06.674
And then the other question   you
offhandedly mentioned, it was repr(Rust).

00:23:06.954 --> 00:23:12.074
So that's r e p r parenthesis Rust
with a capital for some reason.

00:23:12.284 --> 00:23:14.284
And we have repr(C) as well.

00:23:14.294 --> 00:23:16.124
And we have other ones that I forget.

00:23:16.628 --> 00:23:20.193
What that does is it defines the
layouts of structs, if I'm correct.

00:23:20.203 --> 00:23:20.993
It defines the ABI.

00:23:21.163 --> 00:23:24.973
So if you have something that's repr(C),
that is well defined, that is not

00:23:24.973 --> 00:23:28.293
going to change, that is what we use
for FFI, foreign function interface.

00:23:28.331 --> 00:23:32.488
But repr(Rust) can change from one,
even across two compiles, there's no

00:23:32.488 --> 00:23:33.779
guarantees that it will stay the same.

00:23:34.325 --> 00:23:36.535
<v James Munns>Yeah it means you're
not allowed to know, it at least

00:23:36.535 --> 00:23:40.765
are the rules of repr(Rust) so you
can't rely on that but using the

00:23:40.775 --> 00:23:45.720
offset of macro says: whatever the
compiler thinks this layout is, use

00:23:45.720 --> 00:23:47.578
that offset, it's out   of my hands.

00:23:47.600 --> 00:23:51.560
What we're trying to do here is, instead
of using the visitor pattern, we're

00:23:51.590 --> 00:23:53.860
turning serde into a stack machine.

00:23:54.160 --> 00:23:55.580
This is where we get back to forth.

00:23:55.580 --> 00:23:58.900
Forth is an interpreted programming
language that is very simple.

00:23:58.910 --> 00:24:00.310
It has very few primitives.

00:24:00.650 --> 00:24:01.780
It has no local data.

00:24:01.800 --> 00:24:03.770
The only local data is the stack.

00:24:03.800 --> 00:24:08.870
And, the way functions are defined is
essentially as arrays of other functions.

00:24:09.080 --> 00:24:14.270
So, you name an array of function
calls, and that becomes a new function.

00:24:14.560 --> 00:24:17.620
And you don't have to worry about
passing data to functions, because there

00:24:17.620 --> 00:24:19.340
is no way to pass data to functions.

00:24:19.570 --> 00:24:20.960
There is only the stack.

00:24:21.330 --> 00:24:25.050
And so it ends up looking a lot like
recursion, but you sort of manage

00:24:25.050 --> 00:24:31.360
it by the VM itself, rather than the
compiler creating this machine for you.

00:24:31.720 --> 00:24:36.150
So we turn this into essentially
a stack machine, which is where

00:24:36.150 --> 00:24:37.970
the name postcard-forth came from.

00:24:38.283 --> 00:24:41.345
And of our stack machine or
our interpreter, if you want to

00:24:41.345 --> 00:24:44.835
call it that, the input is the
list of offsets and functions.

00:24:45.165 --> 00:24:48.845
So it's got essentially a to do
list of: okay, for each of these

00:24:48.855 --> 00:24:53.335
offsets for serialization, I need
to take my base pointer, increment

00:24:53.335 --> 00:24:56.815
it by this much, and then call this
function with that base pointer.

00:24:57.035 --> 00:25:01.321
And that function is going to know
that it's expecting a pointer to a u8.

00:25:01.341 --> 00:25:06.867
So it's going to cast that void* or
*const () type back to the correct type

00:25:07.207 --> 00:25:11.737
and go: okay, that's a pointer to a u8,
read that and encode it onto the wire.

00:25:12.026 --> 00:25:16.346
And that becomes our output is just
some kind of stream of serialized bytes.

00:25:16.356 --> 00:25:20.646
So it could be to a vec, it could be
filling up a slice and then keeping

00:25:20.646 --> 00:25:23.776
track of how many you've written to
that or however you'd like to, but

00:25:23.776 --> 00:25:27.836
you get some kind of stream output
primitive that you can use there.

00:25:28.016 --> 00:25:30.136
As I mentioned, this is
basically an interpreter.

00:25:30.226 --> 00:25:34.546
We're interpreting the definition
that we generated at compile time

00:25:34.576 --> 00:25:39.220
of all these fields and walking
through that as if we were a Python

00:25:39.220 --> 00:25:40.570
interpreter or something like that.

00:25:40.830 --> 00:25:43.760
And our output just goes
to the output stream.

00:25:44.430 --> 00:25:47.770
And Forth as an interpreter
says that "data is code."

00:25:47.810 --> 00:25:52.260
One of the LISPs said "code is data" and
Forth says the opposite: "data is code."

00:25:52.270 --> 00:25:55.853
You can interpret any chunk of
data as if it was code, if you

00:25:55.853 --> 00:25:57.393
write code that can handle that.

00:25:57.463 --> 00:26:01.523
And that's exactly what we're doing
here is we're generating data instead

00:26:01.523 --> 00:26:05.603
of code at compile time, and then we
write one interpreter that knows how to

00:26:05.603 --> 00:26:07.753
turn that data into data on the wire.

00:26:07.803 --> 00:26:08.633
<v Amos Wenger>I'm gonna stop you there.

00:26:08.683 --> 00:26:11.993
Again, it's interesting because you went
the exact opposite direction that I did.

00:26:12.023 --> 00:26:14.743
I also was like, "Well,
I like serde, but..."

00:26:15.353 --> 00:26:18.283
But then the direction I was in,
instead of doing the stack manually,

00:26:18.493 --> 00:26:21.606
I took the one stable Rust feature
that generates a state machine

00:26:21.626 --> 00:26:23.356
for you, which is async functions.

00:26:23.751 --> 00:26:27.551
And I just did that, and that also
solves the infinite recursion problem.

00:26:27.551 --> 00:26:30.091
But I'll present my take on
it in a different episode.

00:26:30.101 --> 00:26:31.054
It's so interesting!

00:26:31.064 --> 00:26:32.824
You're like, "Let's do
everything manually."

00:26:32.924 --> 00:26:34.684
I bet you don't even have
CodeGen for your thing.

00:26:34.684 --> 00:26:36.924
I bet you've been writing those
arrays manually, haven't you?

00:26:36.965 --> 00:26:37.873
<v James Munns>I did at first.

00:26:37.923 --> 00:26:39.715
Now I have a macro that does that.

00:26:39.765 --> 00:26:42.056
So I actually do have a
version of this that works.

00:26:42.086 --> 00:26:45.354
Not everything works perfectly and
only for some subset of works, but-

00:26:45.354 --> 00:26:47.244
<v Amos Wenger>Is that against
the rules of the podcast?

00:26:47.244 --> 00:26:50.260
Like, we can only present far out
ideas that don't actually work?

00:26:50.260 --> 00:26:52.653
<v James Munns>I've been talking about
postcard RPC and postcard RPC exists.

00:26:52.653 --> 00:26:54.143
It's just something I keep changing.

00:26:54.385 --> 00:26:55.875
<v Amos Wenger>So you keep claiming!

00:26:55.895 --> 00:26:58.165
All I see is, like,
screenshots on Bluesky.

00:26:58.195 --> 00:26:58.895
I don't, I don't know.

00:26:58.975 --> 00:27:00.045
I'll believe it when I see it.

00:27:00.355 --> 00:27:01.015
<v James Munns>It counts.

00:27:01.215 --> 00:27:02.175
So why is this good?

00:27:02.352 --> 00:27:07.302
One good thing is that there's only ever
one serialization deserialization machine.

00:27:07.472 --> 00:27:10.502
It doesn't get monomorphized for
every single version of that.

00:27:10.852 --> 00:27:14.592
And serde generates a very simple,
easy to optimize chunk of code, but it

00:27:14.602 --> 00:27:16.822
generates one of those for everything.

00:27:17.175 --> 00:27:21.595
And in this, there is just one:
the VM that handles whatever

00:27:21.595 --> 00:27:22.685
data that you throw at it.

00:27:22.785 --> 00:27:24.565
And it is an explicit stack machine.

00:27:24.645 --> 00:27:28.595
You can have a stack in there, and as
you traverse down the stack, you can

00:27:28.595 --> 00:27:33.565
say, "Ope, my max stack is 16," and
if you ever try to push a 17th item

00:27:33.565 --> 00:27:36.605
onto the stack, the stack machine
just goes, "Oops, out of stack!

00:27:36.775 --> 00:27:37.965
Serialization failed."

00:27:38.235 --> 00:27:40.435
Which is a challenge sometimes,
particularly when you're

00:27:40.435 --> 00:27:42.255
deserializing unbounded types.

00:27:42.305 --> 00:27:45.575
If you're deserializing a vec of enums
that can contain a vec of enums, that

00:27:45.575 --> 00:27:48.955
can- you can get sort of like that
zip bomb problem when it comes to

00:27:48.955 --> 00:27:53.760
deserialization and serdeJSON has some
workarounds for that and serde itself

00:27:53.760 --> 00:27:56.650
has some workarounds for that where
it'll try not to allocate too much.

00:27:56.710 --> 00:28:00.590
It's  a hard problem when you don't
have bounded types when you are doing

00:28:00.590 --> 00:28:05.747
recursion because the compiler has
no idea what your stack limit is at

00:28:05.747 --> 00:28:09.019
your runtime and it will just go until
the operating system says, "Fail!

00:28:09.069 --> 00:28:09.649
Dead."

00:28:09.672 --> 00:28:10.062
<v Amos Wenger>Yeah.

00:28:10.117 --> 00:28:12.187
<v James Munns>And then your program
ends, which is not a very good

00:28:12.197 --> 00:28:13.702
denial of service protection.

00:28:13.771 --> 00:28:17.331
<v Amos Wenger>If you just have like one
very, very large type, they have to

00:28:17.331 --> 00:28:20.701
be cautious because they don't know
how much stack one function will take.

00:28:20.701 --> 00:28:23.269
If it has big locals,
then it's a fat frame.

00:28:23.692 --> 00:28:27.092
So they have to leave sort of a red zone
of like: okay, we assume that we can

00:28:27.092 --> 00:28:32.195
call at least one more level recursion,
everything related to stack bounding

00:28:32.205 --> 00:28:36.295
in serde is a huge hack, including
the one that lets you technically

00:28:36.400 --> 00:28:38.490
recurse infinitely called stacker.

00:28:38.490 --> 00:28:39.960
I don't know if you were
going to mention that.

00:28:40.010 --> 00:28:40.350
Yeah.

00:28:40.370 --> 00:28:41.080
<v James Munns>Oh yeah, yeah.

00:28:41.220 --> 00:28:43.400
<v Amos Wenger>Just like resize
the stack and then swap to it.

00:28:43.433 --> 00:28:44.324
As far as I know.

00:28:44.366 --> 00:28:44.746
<v James Munns>Yeah.

00:28:44.906 --> 00:28:45.426
I've seen that.

00:28:45.426 --> 00:28:46.666
I have no idea how it works.

00:28:46.782 --> 00:28:47.832
<v Amos Wenger>It's terrifying!

00:28:47.862 --> 00:28:50.193
It's just straight up
changing the stack pointer.

00:28:50.256 --> 00:28:53.253
<v James Munns>And embedded, if you run out
of stack, you just run into your global

00:28:53.253 --> 00:28:54.633
variables and start corrupting them.

00:28:54.857 --> 00:28:57.216
<v Amos Wenger>Yeah, no, it's like
detecting you're going to run out

00:28:57.216 --> 00:28:59.696
of stacks and making a bigger stack,
switching to it, and then I guess

00:28:59.696 --> 00:29:01.106
switching back before you unwind?

00:29:01.116 --> 00:29:04.696
I'm just terrified what happens
if you switch stacks and then you

00:29:04.896 --> 00:29:08.287
recurse back- I don't know, you,
you, something, `setjmp/longjmp`?

00:29:08.307 --> 00:29:08.922
Like something's-

00:29:08.922 --> 00:29:09.559
<v James Munns>No idea.

00:29:10.174 --> 00:29:11.694
<v Amos Wenger>Somehow GOTO into C code?

00:29:11.714 --> 00:29:13.574
I don't know how that would
work, but that's terrifying.

00:29:13.795 --> 00:29:15.695
<v James Munns>So, like I said, I
have a version of this that at least

00:29:15.705 --> 00:29:17.685
for some values of works, works.

00:29:17.685 --> 00:29:20.385
And the interesting thing is that
there's a really good serialization

00:29:20.385 --> 00:29:24.585
and deserialization benchmark
written by the author of rkyv.

00:29:24.805 --> 00:29:27.725
It's called the Rustc Serialization
Benchmark or something like that.

00:29:27.725 --> 00:29:31.245
It's something that I've used a lot and
was very helpful for tuning Postcard

00:29:31.245 --> 00:29:35.480
in that it has a couple of fairly large
datasets for log files and structured

00:29:35.480 --> 00:29:39.699
save files from Minecraft and then
tessellation things It exercises a

00:29:39.699 --> 00:29:42.359
pretty good set of what serde can do.

00:29:42.359 --> 00:29:45.779
And it's used as sort of a semi
objective benchmark between different

00:29:45.779 --> 00:29:48.749
formats, both in size, as well as speed.

00:29:48.799 --> 00:29:52.639
But I managed to get just enough
of postcard-forth working so that I

00:29:52.639 --> 00:29:57.959
could implement the same test sets
from this benchmark on this, and

00:29:57.959 --> 00:30:01.979
then compare it against postcard with
serde versus postcard with forth.

00:30:02.155 --> 00:30:06.222
And so these are the four different
benchmarks, and on all but one of the

00:30:06.222 --> 00:30:09.672
serialized benchmarks- these are the
relative ones, but if you go to the

00:30:09.672 --> 00:30:12.032
repo, there's also the objective ones.

00:30:12.062 --> 00:30:16.686
In the first run, regular postcard is 67
percent of the speed of postcard-forth.

00:30:17.076 --> 00:30:21.086
In the second test, postcard is
faster, and so postcard-forth is

00:30:21.086 --> 00:30:25.836
only 84 percent of the fastest, but
in the other two, postcard-forth

00:30:25.856 --> 00:30:28.186
is 100 percent and postcard is 67.

00:30:28.516 --> 00:30:33.774
Basically, it beats it by like 25
to 50 percent on serialization,

00:30:33.784 --> 00:30:36.244
although it does lose by 15
percent in one of the four tests.

00:30:36.254 --> 00:30:40.294
And then on deserialization,
it's between 10 and 50 percent

00:30:40.314 --> 00:30:41.724
of the runtime of the other one.

00:30:41.774 --> 00:30:45.004
Of this very limited test set,
on seven of eight tests, it's

00:30:45.004 --> 00:30:47.004
faster than postcard with serde.

00:30:47.224 --> 00:30:49.813
And in the one that it's slower,
it's only a little bit slower.

00:30:49.993 --> 00:30:51.063
<v Amos Wenger>That is impressive.

00:30:51.343 --> 00:30:54.160
I've had this question in mind:
you had pointers to functions-

00:30:54.719 --> 00:30:56.353
are those only for postcard?

00:30:56.443 --> 00:30:58.403
How would you make that
work for more formats?

00:30:58.413 --> 00:30:59.813
Would you just derive different tables?

00:31:00.007 --> 00:31:00.787
<v James Munns>Yeah, that's a good question.

00:31:00.797 --> 00:31:01.277
I wouldn't.

00:31:01.457 --> 00:31:02.020
<v Amos Wenger>Okay, cool.

00:31:02.020 --> 00:31:02.502
Just checking.

00:31:02.545 --> 00:31:03.615
You'd know I had to ask.

00:31:03.679 --> 00:31:05.179
<v James Munns>Yeah, yeah,
no, it's very reasonable.

00:31:05.209 --> 00:31:07.149
And this is that like
cheating by doing less.

00:31:07.389 --> 00:31:10.939
I'm cheating by only supporting one format
that I know the qualities of in and out.

00:31:11.069 --> 00:31:13.229
So I know postcard can run through this.

00:31:13.279 --> 00:31:15.599
You maybe could make it work
with JSON and things like that.

00:31:15.609 --> 00:31:19.559
And that would be interesting, but
that's not something I've tried to do.

00:31:19.659 --> 00:31:23.239
Maybe it could be possible by just
replacing essentially the set of standard

00:31:23.239 --> 00:31:27.179
functions, but it would be much more
invasive than it would in serde for sure.

00:31:27.548 --> 00:31:29.629
Cause it's not built with that
degree of freedom in mind.

00:31:30.049 --> 00:31:32.509
<v Amos Wenger>I feel like it's a fair
comparison though, because serde is

00:31:32.869 --> 00:31:34.719
uncompromising in terms of performance.

00:31:34.739 --> 00:31:37.639
It's like, "We will monomorphize
and inline all the things.

00:31:37.949 --> 00:31:40.799
We're trying to be as fast as possible,
even if you're paying compile times.

00:31:40.809 --> 00:31:42.094
There's just no way to opt out of that.

00:31:42.533 --> 00:31:45.205
<v James Munns>And the other thing
that's very relevant for me, especially

00:31:45.205 --> 00:31:48.475
for embedded is that initial testing
shows that it's usually smaller,

00:31:48.745 --> 00:31:52.515
both in the amount of code that you
generate and the actual binary size.

00:31:52.590 --> 00:31:53.290
<v Amos Wenger>Usually?

00:31:53.290 --> 00:31:54.210
What do you mean usually?

00:31:54.410 --> 00:31:54.890
<v James Munns>I say usually..

00:31:54.890 --> 00:31:57.365
Actually in all my tests, it was
smaller and faster, but you know-

00:31:58.500 --> 00:31:59.811
<v Amos Wenger>How could it be?

00:31:59.905 --> 00:32:03.085
<v James Munns>It's not an exhaustive test,
you know, lies, damn lies and benchmarks.

00:32:03.384 --> 00:32:07.891
To control for this, I wrote a code
generation tool that would generate

00:32:07.911 --> 00:32:11.738
types that were arbitrarily deeply
nested, so they would reference

00:32:11.738 --> 00:32:12.918
each other and stuff like that.

00:32:13.319 --> 00:32:16.759
And then I would build an embedded
binary because it's no standard, I can

00:32:16.759 --> 00:32:18.399
control what goes into it much more.

00:32:18.739 --> 00:32:22.959
Which means I can have the baseline where
I do no serialization and deserialization.

00:32:23.239 --> 00:32:27.418
And then I just have a format that
reads data in from the PC, deserializes

00:32:27.428 --> 00:32:28.778
it, and then reserializes it.

00:32:28.818 --> 00:32:30.228
I didn't actually exercise it.

00:32:30.228 --> 00:32:32.658
I can see what the real code size
is because I know nothing's being

00:32:32.658 --> 00:32:36.048
optimized out because it's reading
in from the PC, so it can't assume

00:32:36.048 --> 00:32:37.348
that anything can be optimized out.

00:32:37.398 --> 00:32:40.958
My baseline firmware image that does
nothing but I think say "Hello World!"

00:32:40.978 --> 00:32:45.034
And read data from the PC is something
like 10 kilobytes and the whole project

00:32:45.034 --> 00:32:47.744
is 53 lines of code when I expand it.

00:32:47.892 --> 00:32:50.642
I've tweaked postcard-forth because
I was trying to figure out like,

00:32:50.942 --> 00:32:52.412
is it better to do this or this?

00:32:52.432 --> 00:32:57.052
But in most of the cases- for example,
just the baseline postcard serde and

00:32:57.052 --> 00:33:02.112
postcard-forth for 512 of these nested
data types that might call each other.

00:33:02.492 --> 00:33:08.684
The compiled code size for serde was
640,000 bytes, and the compiled code

00:33:08.684 --> 00:33:13.194
size for postcard-forth was 395,000
bytes, so like two thirds of the size.

00:33:13.644 --> 00:33:17.114
The actual source code in the file,
including those types that I generated,

00:33:17.114 --> 00:33:18.524
was about 10,000 lines of code.

00:33:18.728 --> 00:33:22.654
Serde turned that into 181,000 lines
of code, and postcard-forth turned

00:33:22.654 --> 00:33:24.274
that into 60,000 lines of code.

00:33:24.524 --> 00:33:26.114
So again, about a third of the size.

00:33:26.594 --> 00:33:30.744
And then when it comes to compile
time, I did measure both the

00:33:30.744 --> 00:33:35.344
total compilation time and just
of this crate using cargo timings.

00:33:35.784 --> 00:33:39.277
And the difference in that
compilation time is about 12

00:33:39.287 --> 00:33:43.347
seconds for postcard-forth and
about 26 seconds for postcard serde.

00:33:43.697 --> 00:33:49.517
And this is like a mean test of generating
512 types where most of them include

00:33:49.517 --> 00:33:50.947
seven or eight of different ones.

00:33:50.997 --> 00:33:52.477
It's about as bad as it can get.

00:33:52.487 --> 00:33:57.382
But in this stress test, the sizes are
typically about a third of generated

00:33:57.382 --> 00:34:02.717
code, about two thirds of the actual
compiled binary size after optimizations.

00:34:03.067 --> 00:34:06.527
And it seems to be fairly consistent
when I run it against a bunch

00:34:06.527 --> 00:34:08.037
of different generated types.

00:34:08.188 --> 00:34:09.378
<v Amos Wenger>Did you go
back to your customer and

00:34:09.378 --> 00:34:11.193
have   them try postcard-forth?

00:34:11.798 --> 00:34:12.268
<v James Munns>No!

00:34:12.308 --> 00:34:13.208
No, no, no, no.

00:34:14.148 --> 00:34:15.148
They do not need this.

00:34:15.428 --> 00:34:16.762
They're doing this on a desktop.

00:34:16.823 --> 00:34:18.513
<v Amos Wenger>The public
wants to know, James!

00:34:18.533 --> 00:34:19.168
<v James Munns>Yeah, yeah.

00:34:19.168 --> 00:34:22.253
I don't experiment everything in my
customers unless it's really good.

00:34:22.253 --> 00:34:23.323
We're still on the positives.

00:34:23.537 --> 00:34:26.087
For deserialization, it'd be
very easy to do this in place.

00:34:26.367 --> 00:34:30.249
Because if we wanted to deserialize,
we could have that base pointer just be

00:34:30.269 --> 00:34:35.096
a maybe uninit, in the return location
and then use that as the pointer that

00:34:35.096 --> 00:34:36.982
we walk through for deserialization.

00:34:36.992 --> 00:34:39.182
It is built to be done in place.

00:34:39.202 --> 00:34:41.962
So there are never stack copies
because everything is either done

00:34:42.162 --> 00:34:44.382
from the source or to the destination.

00:34:45.072 --> 00:34:46.112
So why is this not good?

00:34:46.332 --> 00:34:48.552
Now, we're getting to the
red words on the slide.

00:34:48.947 --> 00:34:52.017
It's another crate that requires
a derived trait for every type.

00:34:52.067 --> 00:34:55.717
Unless we get reflection in Rust,
there's no way to get these offsets and

00:34:55.717 --> 00:34:57.627
map them to a pointer for every type.

00:34:57.647 --> 00:35:01.347
If we get reflection, maybe this won't
be a downside, but we don't, which means

00:35:01.347 --> 00:35:04.647
this is another trait that you have
to PR to all of your favorite types,

00:35:04.697 --> 00:35:09.137
like chrono or nalgebra or whatever,
where you want them to conditionally

00:35:09.157 --> 00:35:11.491
implement some another derived type.

00:35:11.641 --> 00:35:14.081
Serde is great because the
whole ecosystem uses it.

00:35:14.361 --> 00:35:17.731
So if it doesn't implement serde
already, pretty much everyone knows

00:35:17.731 --> 00:35:20.111
what it is and they're going to
just hit merge on that PR anyway.

00:35:20.254 --> 00:35:24.089
Without mentioning all of the type
erased void* pointers, the engine

00:35:24.089 --> 00:35:28.809
itself for doing this serialization
or deserialization is wildly unsafe.

00:35:28.859 --> 00:35:32.429
I think it could be done correctly and
you could test it with Miri, you could

00:35:32.429 --> 00:35:35.855
do that, but it's essentially like a
raw scripting interpreter that you're

00:35:35.895 --> 00:35:39.255
plugging into your program, and if I
get something wrong then it'll do out

00:35:39.255 --> 00:35:40.995
of bound writes and do terrible things.

00:35:41.005 --> 00:35:44.055
Serde has the benefit, I think,
almost entirely safe code.

00:35:44.135 --> 00:35:48.065
It might be entirely safe code, because
it's just generating very simple

00:35:48.065 --> 00:35:50.103
Rust code that optimizes very well.

00:35:50.443 --> 00:35:52.483
This is generating not that.

00:35:52.493 --> 00:35:55.473
It is doing pointer casting, it
is doing additions to pointers

00:35:55.643 --> 00:35:56.703
and reinterpreting them.

00:35:56.763 --> 00:36:00.293
It's doing all the nasty tricks that
are fun to look at, but when I say it's

00:36:00.303 --> 00:36:03.103
wildly unsafe, it is wildly unsafe.

00:36:04.123 --> 00:36:07.278
And the other thing is that manually
implementing these serialization and

00:36:07.278 --> 00:36:08.963
deserializing traits is wildly unsafe.

00:36:09.183 --> 00:36:12.573
If you have the derive macro do it,
and it gets the offset of correctly

00:36:12.573 --> 00:36:14.388
and things like that: totally fine.

00:36:14.398 --> 00:36:17.248
It will work as long as there's
no bugs in the derived macro.

00:36:17.888 --> 00:36:20.818
If someone tries to manually
implement that, like they can manually

00:36:20.828 --> 00:36:24.548
implement serde today, you could
just put an offset of a million and

00:36:24.548 --> 00:36:27.598
then you're walking off into some
other heap thing or you segfault.

00:36:27.598 --> 00:36:30.438
It is like a designed exploit in a box.

00:36:30.488 --> 00:36:33.008
I don't know how I could design
it worse to be exploitable.

00:36:33.028 --> 00:36:33.718
You know what I mean?

00:36:33.928 --> 00:36:36.198
<v Amos Wenger>I mean, that's why
we have unsafe traits, right?

00:36:36.226 --> 00:36:37.966
<v James Munns>Yeah, it would
be an unsafe trait for sure.

00:36:38.273 --> 00:36:41.123
We also still need some code
to get to Data model Types.

00:36:41.123 --> 00:36:42.652
You need some monomorphization.

00:36:42.662 --> 00:36:46.391
Like I mentioned way earlier, a
hash set gets represented as an

00:36:46.391 --> 00:36:50.544
array on the wire, but there's no
like slice inside of the hash set.

00:36:50.784 --> 00:36:54.947
We would need to do something to
go to an iterator to then each

00:36:54.967 --> 00:36:56.087
element and things like that.

00:36:56.087 --> 00:37:01.327
So there might still be some recursion
or function calls or some kind of

00:37:01.327 --> 00:37:04.216
monomorphization that's required
to get you to that data type.

00:37:04.306 --> 00:37:05.626
<v Amos Wenger>Right, what about arrays?

00:37:05.676 --> 00:37:07.818
How do arrays work with postcard-forth?

00:37:08.408 --> 00:37:12.608
<v James Munns>Right now, I think I
just have a function that takes them

00:37:12.608 --> 00:37:14.018
as a slice or something like that.

00:37:14.028 --> 00:37:17.778
Like there's still some cheating for
sure in there, but in my test, I only use

00:37:17.778 --> 00:37:22.474
like strings and vecs of u8s and things
like that,  they end up becoming that.

00:37:23.051 --> 00:37:25.332
Yeah, especially for iterators-
basically things that you can't

00:37:25.332 --> 00:37:27.562
deref to a data model type.

00:37:27.562 --> 00:37:31.062
And for that in place deserialization,
we need a whole new RFC because

00:37:31.062 --> 00:37:35.152
there's today no way to set the
discriminant unsafely for an enum.

00:37:35.712 --> 00:37:38.362
So right now the only way to
deserialize an enum would be to

00:37:38.512 --> 00:37:41.662
deserialize it onto the stack and
then copy it to the destination.

00:37:41.741 --> 00:37:45.540
I have an RFC up to change that,
but it's an RFC  .It hasn't even

00:37:45.550 --> 00:37:46.710
started to be implemented yet.

00:37:46.710 --> 00:37:48.757
<v Amos Wenger>Yeah, you're
the one who wrote it up.

00:37:48.997 --> 00:37:49.727
When did you?

00:37:49.817 --> 00:37:50.516
Last week!

00:37:50.848 --> 00:37:51.948
<v James Munns>So it comes out of this.

00:37:51.958 --> 00:37:55.028
I have a blog post a while back and it
was while I was doing this is I realized

00:37:55.028 --> 00:37:57.368
there was no way to do that for enums.

00:37:57.618 --> 00:38:00.368
I could do all the spooky stuff
for structs and raw data types,

00:38:00.368 --> 00:38:03.628
but enums, you have to make them
on the stack and then copy them.

00:38:03.628 --> 00:38:06.814
And that's why that perf table had
like a no enums field because I was

00:38:06.814 --> 00:38:10.374
trying to figure out how bad that
was when compared to other types.

00:38:10.461 --> 00:38:12.251
And it's not quite a
linear program either.

00:38:12.261 --> 00:38:15.961
So when I say that it's a program and you
just run top to bottom: Uh, turns out no,

00:38:15.961 --> 00:38:21.011
you need control flow too, because even
just data types are complex programs.

00:38:21.261 --> 00:38:22.731
For enums, you need branching.

00:38:22.741 --> 00:38:26.131
You are only serializing one
of possible choices, or only

00:38:26.141 --> 00:38:28.191
deserializing one of possible choices.

00:38:28.871 --> 00:38:32.331
And we need loops, because you have slices
and iters, and you have to go through

00:38:32.331 --> 00:38:36.851
them, and you're not going to inline every
possible length of every possible type.

00:38:36.931 --> 00:38:41.068
It really is a whole interpreter
that not just needs control flow-

00:38:41.077 --> 00:38:43.063
<v Amos Wenger>You're gonna have to
design your own bytecode, and then

00:38:43.063 --> 00:38:46.153
the array is gonna be like type of
instruction, and then a few operands,

00:38:46.163 --> 00:38:49.083
and it could be a function address,
it could be something else...

00:38:49.083 --> 00:38:49.293
<v James Munns>Yeah.

00:38:49.383 --> 00:38:50.931
<v Amos Wenger>Is that
even Rust at this point?

00:38:51.011 --> 00:38:52.201
Does that count as Rust?

00:38:52.370 --> 00:38:55.096
<v James Munns>Forth has some ways that
you handle that, but that's probably

00:38:55.096 --> 00:38:56.386
a whole talk for another time.

00:38:56.689 --> 00:38:58.229
So then the question is: is it worth it?

00:38:58.229 --> 00:38:59.509
It is wildly unsafe.

00:38:59.539 --> 00:39:01.919
It is questionably implementable today.

00:39:02.179 --> 00:39:05.379
And you'll still end up with in
some metrics a much worse version

00:39:05.379 --> 00:39:08.609
of serde, despite being faster
and smaller and things like that.

00:39:08.629 --> 00:39:11.019
And really the answer
is: I have no idea yet.

00:39:11.119 --> 00:39:14.479
But it was something that I spent
a lot of time on and I thought

00:39:14.479 --> 00:39:15.589
you might find interesting.

00:39:15.589 --> 00:39:16.139
So, uh...

00:39:16.139 --> 00:39:16.979
<v Amos Wenger>I sure do.

00:39:17.051 --> 00:39:19.679
<v James Munns>We'll see if some
calculus changes with something

00:39:19.709 --> 00:39:20.839
that makes it really worth it.

00:39:21.224 --> 00:39:25.484
<v Amos Wenger>I mean, this is a
perfect setup for my take on another

00:39:25.484 --> 00:39:29.954
serde, which we don't have time
for today, but this is so fun.

00:39:30.160 --> 00:39:32.870
You took all the other choices, I'm so
glad we're doing this together, James.

00:39:33.715 --> 00:39:35.685
<v James Munns>I'm excited to see yours
now, cause you've talked about yours.

00:39:35.685 --> 00:39:36.875
I think a bit more publicly...

00:39:36.901 --> 00:39:39.301
<v Amos Wenger>Yeah, but I wanted
to make sure it was useful before

00:39:39.301 --> 00:39:40.081
I actually talked about it.

00:39:40.109 --> 00:39:41.519
I never knew when the time was  right.

00:39:41.839 --> 00:39:42.805
<v James Munns>Oh, I went the opposite.

00:39:48.027 --> 00:39:50.307
This episode is sponsored by CodeCrafters.

00:39:50.495 --> 00:39:53.765
CodeCrafters is a service for
learning programming skills by doing.

00:39:54.435 --> 00:39:57.725
CodeCrafters offers a curated list
of exercises for learning programming

00:39:57.725 --> 00:40:00.735
languages like Rust or learning
skills like building an interpreter.

00:40:01.210 --> 00:40:04.750
Instead of just following a tutorial, you
can instead clone a repo that contains

00:40:04.760 --> 00:40:08.490
all of the boilerplate already, and make
progress by running tests and pushing

00:40:08.490 --> 00:40:11.830
commits that are checked by the server,
allowing you to move on to the next step.

00:40:12.470 --> 00:40:15.440
If you enjoy learning by doing,
sign up today using the link at

00:40:15.440 --> 00:40:19.560
sdr-podcast.com/codecrafters,
or use the link in the show

00:40:19.560 --> 00:40:20.790
notes to start your free trial.

00:40:21.210 --> 00:40:23.730
If you decide to upgrade, you'll
get a discount and a portion of

00:40:23.730 --> 00:40:25.270
the sale will support this podcast.

00:40:25.715 --> 00:40:29.405
That's sdr-podcast.com/codecrafters.

00:40:30.105 --> 00:40:32.485
Thanks to CodeCrafters for
sponsoring this episode.

