My first real code in Rust
Every language starts with a playground. You CAN do amazing things there, but they are as amazing as a neighboring kid sand cake.
As soon as you move from playground into ‘do something real’ you move from sand cake realm into ‘paper applications with a glue and pine cones’, which are more yielding, but still ‘kids’ level.
Commiting to an existing applications or libraries is a very different kind of activity. Amount of ‘small but important things’ is staggering, and real code is not bound by explanation simplicity.
So, I’ve made my small first commit (PR, to be precise) to a library I use. The library is Speedy2D, and I found that type Vector2
(which is a simple tuple of two numbers) lacks assign operation (f.e. x+=y
). I decided to add them. The code is very simple but the process is interesting. Here is my PR #66.
What I found
First, Speedy2D is using a rather unusual code style.
The usual
fn foo(){
...
}
become
fn foo()
{
...
}
I don’t know reasons except for old C nostalgia for that.
Second, I found justfile, which is a script for just. This is first time I heard about it. It’s a modern upgrade to make
without .phony
madness, and it’s definitively within my work-related interests, so I added it to my learn list.
Also, writing code was unexpectedly nuanced. I though it would be a trivial two-liner (self.x+=rhs.x
; self.y+=rhs.x
), but I found a bit more on type side.
This is code for one of the functions to implement +=
operation.
impl<T: Copy + std::ops::AddAssign, R: Into<Vector2<T>>> std::ops::AddAssign<R>
for Vector2<T>
{ #[inline]
fn add_assign(&mut self, rhs: R)
{
let rhs = rhs.into();
self.x += rhs.x;
self.y += rhs.y; }}
I wrote it heavily prying into surrounding code.
Look at type expressions!
We are working with Vector2
type, which is parametrized by type T
.
We are implementing std::ops::AddAssign
trait for Vector2<T>
. It contains add_assign
function which takes a mutable reference to self
and a second value rhs
with type R
, which we require to be (interesting part starts here) convertable into T
.
I was pleased and perplexed. Instead of requiring that both types are the same, we are allowing to work with different types, with condition that second type can be converted into first one via into
function. It’s a really, really clever thing to do, as it does not restrict normal operations (when all numbers are the same type) but enhance of correct expressions with other types. .into
is eliminated for the same type conversion (when T::into(t:T)
is called), so it do not create any overhead (except for buttons pushed).
Also, there is Copy
for T
(I’m not completely sure why it’s here. It’s a requirement for T
, the inner type for Vector2
, so we want it to have cheap copy? Why?).
Finally, the thing I was interested in is testing. I expected a long long list of tests, but I found that there is a two-fold test system:
- unit tests (the normal tests with ‘
#[test]
’ directive). - integration tests (which are called just ‘tests’ in the lib) with headless opengl context.
Integration tests
The integration tests are usually the most lore-full, with million small nuances and clutches.
#[cfg(not(all(target_arch = "x86_64", target_os = "linux")))]
compile_error!("The automated tests currently support Linux x86_64 only");
That pushes me very much away from Rust I know to some ‘other Rust’ for compile-time expressions. core::compile_error
is a rather funny, and it come without source code (inside standard library):
#[stable(feature = "compile_error_macro", since = "1.20.0")]
#[rustc_builtin_macro]
#[macro_export]
#[cfg_attr(not(test), rustc_diagnostic_item =
"compile_error_macro")]
macro_rules! compile_error {
($msg:expr $(,)?) => {{ /* compiler built-in */ }};
}
To be honest cfg(not(all
is kinda… jinji. I get a lot those roundabout expressions is Jinja/Ansible, and they usually are not very pleasant to use or read.
The more interesting in tests is the way they are declared. (I’m not sure, may be this is the usual thing for Rust tests, but I missed it when I was reading docs).
There is main
function which initialize EventLoop
(wow), gather tests by pushing closures into vector of tests). Basically, all tests are list of closures inside main
function. Why is event loop there? Why not a straight function call? Is it to mock something? Why tests are in this form instead of a small stand-alone programs? I don’t feel the pain they are solving (All integration code is solving someone’s pain. Or adding.).
I feel it not a really good test system, because tests are failing on the first failed assert without running the rest of tests.
I, actually, found that one of the tests is failing on my machine, and fixing those tests is much more interesting issue for me to play with.
CI
I also peeked into CI workflow for the library. There is a schedule to run it (except for push/PR) and I have no idea why it’s there. To detect code rot? It’s first time I see such approach. I also noticed that tests section is running ‘features-disabled’ tests only. Odd, odd…
I also found no release workflow, it looks like releases are managed manually.
The code is run via xvfb-run
, which solves obvious problem of having X11 server available on CI server (which is VM for Github Actions without hardware access). As my hobby-related Rust side is mostly with graphic applications, I really appreciate the way of testing. (My job is on different side, with zero graphics and max servers, so I feel as a newbie in a realm of ‘graphical desktop applications’).
Changelog
I expected to find some changelog manager like reno
(not sure if I like it or not), but there is none, changelog is completely manual.
justfile
As I said, just
was the main discovery of this endeavor. It’s usually hard to setup CI for everything (like doing automatic releases), so some operations are done manually. “Just” allows to write down procedures for those semi-automatic processes as a code in the repo (yay! less lore! more code!).
For Speedy2D there were two targets: build-example-webgl
and precommit
(with build checks for wasm32-unknown-unknown
platform). I suspect precommit
is for pre-commit git hook.
Judging by amount of wasm32, it causes the most pain, or is the most loved by QuantumBadger (the library author).
Conclusion
I got a glimpse of some production-grade code, which will help me a lot in my own code. I got no review yet, but if no unknown for me things arrives, I’m pretty sure it will be accepted.