Elizafox's Ramblings

The blag/dumping ground of Elizabeth Ōkami

I've been using Rust for a month or so now, and I have some thoughts about it.

Diehard Rustaceans who believe their language is perfect in every way may wanna pass on this one.

Not everything I'm about to say is exactly nice, and I know how bigoted about Rust some of y'all are, and I know that any criticism of Ferris's Chosen Language™ is often swiftly met with rebukes and cries about memory safety and this and that.

Please understand, my criticism is not designed to undermine Rust! My criticism comes from a place of actually caring about the language. I want to see my gripes with it get fixed.

With all that out of the way...

The Good™

Rust does get a lot of things right.

  • Generics are awesome, as are generic traits, GAT's, and such
  • Traits rock; the ability to just extend any type is great
  • Love it or hate it, the borrowing mechanism is helpful in ensuring I never have to worry about use-after-free (unless I drop to unsafe... more on that in a moment)
  • Unsafe is a great escape hatch when you need it
  • The C FFI is pretty good
  • Default immutability makes you think about how you mutate data and side effects in your code, which is a good thing
  • Dependencies and packaging are mostly done right with Cargo, at least, better than I've seen most programming languages (looking at you, Python)
  • There is just enough access control to be useful, and do most useful tasks (like pub(super), pub(crate), pub, and the default of private); private being the default makes you think about what you export
  • Results > exceptions, full stop, this is how C++ should have done it
  • Clippy is one of the best static analysis tools I've ever used, bar none
  • rustc has great diagnostics, probably the best of any language I've ever used
  • Though some complain about it, I love the ? operator
  • The lack of function overloading; 99% of the time, when I wish I had overloading, what I really want is a generic
  • I actually like dyn a lot, the way it does trait-based polymorphism feels very natural to me
  • Concurrency in Rust is pretty good for the most part
  • The ability to easily export things to the C ABI is great
  • Having code work about as fast as the equivalent C/C++ code without much effort whilst being safe is fantastic
  • rustdoc is Fucking Awesome!
  • Rust encourages test-driven development

The Bad™

There's a lot I don't like, though.

  • Rust is a very complex language, more complex than C, and that's saying something (although it isn't a fractal of complexity like C++)
  • Why oh why do we need to explicitly upcast integers, the type system should not shit itself when an upcast is needed
  • Is your string a String, str, [u8], Vec<u8>, CString, OsString?
  • Conversion between string types is not straightforward
  • .as_str() vs to_str()... make up your fucking minds, which is it???
  • Interop with C often requires silly casts and is quite noisy
  • .into() either works or it doesn't, and you'll be left wondering why
  • If you need to build your own copy of the standard library (e.g. with opt-level = "z"), you need nightly
  • panic() should not unwind... why does it unwind wtf?
  • The C++ FFI story is pure 100% shit
  • Too much boilerplate for simple tasks like error handling
  • A lot of good stuff is in nightly only, with stabilisation seemingly not a priority
  • GUI is a horror show
  • Although not as prevalent as it once was, a lot of people are strapped to nightly at the hip, so you might be too
  • str.len() is the number of bytes in the string, which is rarely a useful thing to know, str.size() should be the number of bytes instead
  • Unicode manipulation in Rust is a sad story and you still need external libs; sad because they literally got it right by making strings UTF-8 by default, but gave you very few tools to do anything useful with them
  • Unless you use nightly, you have to repeat yourself with array type sizes: let arr[u8; 3] = [0u8; 3];
  • no_std sucks, a lot, it's borderline unusable outside of embedded and with support libs
  • OOM conditions always just abort, which may not be what you want
  • No default, optional, or keyword arguments
  • No native varargs
  • C varargs are somehow still unstable, so you can't write a C standard library on stable
  • You can't just turn an OsString into a CString, which to me sorta defeats the point of it
  • char is a scalar value, not a codepoint or character
  • If you need to add a lifetime to a struct, you then need to modify everything that touches that struct, including impls
  • As I've posted about before: no container traits
  • macro_rules! are ugly and complex, proc macros are even worse
  • The standard library is a little too minimalist, and depending on third-party crates isn't always fun or even feasible (makes auditing painful)
  • Lack of thread-safe data structures
  • Sometimes you'll randomly find certain things are bottlenecks that you don't expect
  • The borrow checker isn't always smart and doesn't always know when a borrow is safe
  • rustc's static analysis could be improved
  • Clippy should by default emit warnings about using unwrap() willy-nilly without a docstring
  • One real implementation means that the language is chained at the hip to LLVM's whims (yes I know about rustgcc, I look forward to being able to use it in 5-8 years)
  • In general, Rust may be at 1.70 at the time of this writing, but it feels like a language still in beta
  • Loading shared libraries at runtime is sadness and pain
  • The Rust governance structure is opaque
  • Sometimes what I want is the fastest integer data type on my architecture and don't care much about width because I'm not dealing with big numbers; closest you get is std::ffi::c_int, but that's still not quite right and unsatisfactory

The Ugly™

This stuff is really, truly, 100% unacceptable.

  • The Rust Evangelism Strike Force® and the attitude that Thou Shalt Rewrite Everything In Rust for questionable benefits; there is room for many languages in this world
  • The Rust community likes to pretend it invented programming safety; it did not, Lisp has been doing it since 1959
  • The Rust community gives me “Haskell before they looked in the mirror and did something about their toxicity” vibes, they pretend their shit doesn't stink, but trust me... it does
  • Editions are pointless, given you often still wind up depending on a specific rustc version or above
  • Unsafe code in Rust is more painful than it needs to be; lots of spooky undefined behaviour around it
  • If you write a lot of unsafe code (like in a kernel or such), Rust is honestly less ergonomic than C in many ways
  • Interior mutability is painful
  • Pointer provenance rules are largely undefined
  • Pointers in general need an overhaul
  • Stacked borrows with Miri are incredibly complex math-heavy things and aren't really how the language works, trying to understand it has given me migraines
  • Lack of cyclic garbage-collected structures makes implementing things like doubly-linked lists and trees that refer to their parents an absolute irritant without dropping to unsafe; I do know about rust-gc, but it's just not as good at something built-in
  • Speaking of that, I should not have to drop to unsafe to implement a linked list or tree
  • The standard library's linked lists suck, you can't even insert in the middle in O(1) time, without using cursors which are somehow still not stable
  • Rust's evolution is basically corporate-driven, not community-driven
  • SO MANY FUCKING CRYPTO GRIFTER COMPANIES IN THE RUST COMMUNITY, burn them, burn them all to the fucking ground

Conclusion

I still love Rust. I could write a huge list about things I love and hate in C, too. However, my Gods, it could be so much better than it is.

I want to see it get better over time.

I doubt I'm the first to have noticed all these good, bad, and ugly things, certainly.

If you have a constructive argument for or against anything here, let me know what you think on Fedi. I welcome all constructive feedback. 💜

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay

I'm back in Seattle but I got shit to do. This is gonna be a short post.

As per my last post, I was experimenting with doing mangled import from Rust.

I now realise how bad of an idea that is.

Metadata

Every dylib contains metadata that helps elucidate the types of functions, struct layout, MIR code of generics, etc.

Unfortunately, parsing said metadata seems to be a nightmare, and pretty much only the Rust compiler knows how to do it.

Allocators

Turns out Rust dylibs may or may not need an allocator, and that has to be set up if you load something.

Unloading

Unloading a Rust dylib that isn't expecting to be unloaded means you're likely to have a bad time, as things get dropped that aren't expecting to be dropped.

Too many symbols

Rust exports basically everything into a Rust dylib, it doesn't really try to hide things or pare them down. This is pretty awful practice. It pollutes the namespace.

Unstable ABI

The Rust ABI is unstable, and subject to change at any time. Just because names are mangled more or less consistently, doesn't mean that you can just willy-nilly import things. There is more to the ABI than that. Said metadata is part of it as well (the .rustc section of a dylib).

Sigh

I hope one day this becomes a possibility, because this would be a big win.

In the meantime, I recommend checking out the abi_stable family of crates, which can help you include rich Rust in your crates, instead of just exporting a C ABI and making a cdylib.

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay

I'm writing a mangled symbol loader for Rust.

I'm actually soliciting feedback from experienced Rust programmers here, which would be greatly appreciated 💜.

General design

I use a design similar to Boost.DLL's mangled import.

I scan the object file for exported symbols. On Mach-O (macOS and friends), I scan the relevant architecture.

While scanning the relevant exported symbol table, I use rustc-demangle to demangle the symbols. I create a map of mangled symbols to demangled symbols (Note: on Mach-O, I also strip the leading underscore, because macOS and friends prepend an underscore to all symbols in the file).

Once I demangle the symbols, I can then use libc::dlsym or whatever to load a symbol from a string, after looking it up in the map.

Drawbacks

Obviously there are caveats I am aware of.

  • Rust has no stable ABI; this approach is not guaranteed to work between compiler versions, although I trust rustc-demangle will be updated promptly. Nonetheless, everything must be built with the same compiler. I think this would be a problem with just linking with a Rust library in general, unless some sort of backwards compat stuff is in there that I'm not aware of. I should use said mechanism if it exists; let me know if it does! Update: I found out there is in fact metadata I can use.
  • A lot of symbols are exported in a Rust dylib; I don't know which ones are relevant to the functionality of the shared object, or if it's not as simple as just “demangle, map to a mangled symbol, dlsym and you're off to the races.” But so far it all seems to work, based on limited testing.
  • Due to the above issues, I worry this system is brittle. I also question if Boost.DLL isn't also brittle, because ABI has been broken before by GCC and Clang in C++ (they're not even 100% compatible with each other afaik).
  • I don't do parameter checking during the mapping process; in fact, I strip off the hash, which could be used (I think) in theory to do such checks. I haven't implemented a scheme to actually generate this hash, and I don't want to maintain something like this (it would involve parsing Rust code, and I don't want to write a Rust parser). This makes mangled imports unsafe. But this is a problem with importing unmangled functions too, and there's not much we can do about it.
  • The loading process on every platform I'm aware of involves a cast from void* to an extern “Rust” function pointer. Guaranteed to work on Unix by POSIX, and I think Windows (but I'm not 100% sure on that). But I don't know the applicability on things like WASM or AVR. Then again... are shared/runtime loadable objects even a thing on such platforms???

Comments welcome

Let me know what you think on Fedi! I welcome all constructive feedback. I'm sure I haven't thought of everything, and I really want to know what you think. 💜

I do ask that you please bring well-thought out arguments, and not just ad hominems and parroted, cargo culted nonsense.

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay

This is gonna be a short post. I'm tired. I'm in God damned Twin Falls, Idaho. I'm a day away from home.

(Author's note: guess I blew the whole “short” thing, because of course I did)

I want to talk about something Python does bettet than Rust, even whilst not having a 1:1 mapping of the trait concept.

Assumptions

I assume you know what a trait in Rust is; if not, go find out (or by example if you prefer).

I also assume you know what an Abstract Base Class — an ABC — in Python is. If not, here's some quick information.

With that out of the way...

My gripe

The ABC concept doesn't 1:1 map to traits, but there are similarities.

Notably, I want to point to the fact Python's collections have ABC's, but Rust has no base traits for its containers.

Why is this a problem?

A unified interface

In Rust, traits more or less specify an interface.

None of the standard containers implement container traits.

This means there is no unified container interface.

This may not seem like a problem, but it makes for some annoyances.

Genericity in accepted containers

Consider you want to implement something in Rust that takes any mapping container at all.

This is pseudocode-ish, but you should get the idea. The idea here is we extend all mappings with a new method:

trait dyn Map {
    fn replace(&mut self, key: Self::Key, value: Self::Value) {
        self.remove(key);
        self.insert(key, value);
    }
}

Obviously, such a trait like Map would be super useful in this case.

But it doesn't exist!

You have to roll your own (example stolen from Reddit):

use std::collections::{HashMap, BTreeMap};
use std::hash::Hash;

trait MapExtend {
    type Key;
    type Value;
    fn replace(&mut self, k: Self::Key, v: Self::Value);
}

impl<K: Eq + Hash, V> MapExtend for HashMap<K, V> {
    type Key = K;
    type Value = V;
    fn replace(&self, k: Self::Key, v: Self::Value) {
        self.remove(k);
        self.insert(k, v);
    }
}

impl<K: Ord, V> MapExtend for BTreeMap<K, V> {
    type Key = K;
    type Value = V;
    fn replace(&self, k: Self::Key, v: Self::Value) {
        self.remove(k);
        self.insert(k, v);
    }
}

fn main() {
    let mut my_hashmap: HashMap<i32, f32> = HashMap::new();
    my_hashmap.insert(42, 23);
    my_hashmap.replace(42, 69);
    println!("{}", my_hashmap.get(42).unwrap());

    let mut my_btreemap: BTreeMap<i32, f32> = BTreeMap::new();
    my_btreemap.insert(420, 31337);
    my_btreemap.replace(420, 1337);
    println!("{}", my_btreemap.get(420).unwrap());

But this sucks! It means that all containers now have to be bent to work with your funky little trait, rather than there be an official interface. It isn't difficult... but it shouldn't be like this.

And of course, it means tons of boilerplate.

Python's ABC's

As for what Python can do here; you could monkeypatch the relevant ABC (although I don't know if it would affect built-in containers... there's a lot of magick in the system I don't quite understand), but you can also subclass containers as needed.

You can also check if a container matches a type, like so:

from collections.abc import MutableSequence

def rotate_seq_right(seq):
    if not isinstance(seq, MutableSequence):
        raise TypeError("not a MutableSequence")

    elem = seq.pop(0)
    seq.append(elem)

And this will work with any mutable sequence, and give you a diagnostic if your type isn't one.

How did Python, a duck-typed language, do better than Rust here???

Simplified container implementation

In Python, ABC's specify what a container implements. If you inherit from the Mapping ABC, your container is a Mapping, full stop.

This means that extra functionality can be had for free just by inheriting from an ABC, which greatly simplifies container implementations. You can implement any type of tree, any type of mapping at all, you could even make a keyed linked list if you wanted (don't do this, please). By implementing a few methods, your type is first-class with Dict, which is also a mapping.

Instead, you have to implement it all by hand and hope it conforms to the interface users are expecting. Fun!!!

Conclusion

Rust, please implement container traits? Please? It would be super useful and helpful and it would enable everyone to extend all containers easily as well as other funky shenanigans like passing containers as generics without specifying the container.

Pretty please? 🥺🙏

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay

I've made no secret for a long time I have many diagnoses. EUPD, MPD, C-PTSD, Autism/ADHD, DID, those are the big demons.

For too long, it's been a crutch I've leaned upon. I ascribed everything I did wrong to it.

What exactly did I do wrong? I acted out, I made people anxious, I tried to get attention in bad ways, I sometimes overstepped boundaries.

And it was wrong of me to do so. I can only apologise and hold myself accountable, and allow others to do the same.

I will share what's helped me on my journey. I'm ridiculously far from perfect or where I need to be, but I know where to start now. I hope I can help someone along their path.

Ultimately, you must find your own path. All I can share is the path I took, if you wish to follow it. Deviate where you must, but never give up.

Change is hard, but possible

At the end of the day, the only one who can change you, is you. No one else can do it. Not even the best therapist in the world can do it.

A therapist can teach you how to manage your demons, but it's your job, solely, to do so.

Holding yourself accountable is part of that job. Apologising and working hard to change is part of that job too.

It's not easy to change. Old habits die hard. Behaviours which may have been an adaptation to a bad environment take time to unlearn.

Love yourself with all your heart, even if you feel unlovable

Throughout all this, it's important to love yourself. Even if you feel unlovable. Even if you feel like a piece of shit.

Maybe you've hurt people. Most people have. Maybe you've been a piece of shit. At some point, most people have been too. Maybe you've acted out. Most people have done that as well. Worse people have walked the Earth than you, believe me. Much worse.

Don't hold yourself to a standard you wouldn't hold others.

Don't throw yourself a pity party. Do something about it. Check yourself. Learn how to analyse your emotions. Learn how to be mindful. Tell yourself that you're good enough. That doesn't mean you can't be better, but you are sufficient. Even if you don't feel good enough, do it anyway. Remind yourself that you will make mistakes.

Wallowing in sadness achieves nothing. Working hard on yourself does.

Change comes from within. If you truly want to change, don't blame others, but also stop blaming yourself. Blame is not constructive. Recognise your faults as if you're investigating yourself and looking to fix them. It's a problem solving exercise, one which if you're truly dedicated, you'll eventually solve.

To err is to be human

You will fuck up again. You are not perfect. None of us are. You may even make the same mistake repeatedly until you learn. Don't let that stop you.

Hold yourself accountable for your fuckups, and then think, “how will I try to prevent this from happening again?” Analyse your thought processes. Analyse your reasoning. Go over everything with a fine-tooth comb. Think about what needs changing, and integrate it into your routine.

Yes you can

If you hold yourself accountable and work to change your behaviour by being mindful, and recognising when you fuck up, you will eventually change those behaviours. Perfection isn't the goal, improvement is.

Cast out the negativity

Ditch anyone who expects perfection. Ditch anyone holding you to to impossible standards. Ditch anyone who says you can't change. Get rid of the negative people in your life and those who don't really want to see you get better. They will only drag you down. They will not help you grow.

Nothing is worse than a person who wants to hold you back.

You must remove the trash from the garden before flowers will grow.

Better late than never

Improvement and learning is a lifelong process. Change won't happen overnight. It will take time.

But no matter if you're 8 or 80, change is always possible.

You don't stop learning just because you get older. Sure, habits become more difficult to break, but it's never impossible.

If you want to change, deep in your heart, do it now.

You are not alone

Most people have some friends. Find yourself good ones. Surround yourself with those who build you up and are invested in seeing you get better.

If you can't afford or find a therapist (therapists are often hard to find in some places; we are in the midst of a mental health crisis), look up things like Cognitive Behavioural Therapy and Dialectical Behavioural Therapy techniques. It's not perfect, it's not as helpful as a therapist, but it's better than doing nothing.

Don't let the lack of a therapist make you think you can't change.

And don't let backsliding stop you. What matters is you're trending upwards. You may have huge dips in the road, but you need to work past them.

Failure is NOT an option

Don't give up. Never give up, no matter what, no matter how hopeless it seems.

Nothing in your life is unfixable. People have gotten out of worse situations than you.

You can do this. I believe in you. 💜

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay

I was talking with my friend Anna about runtime modularity in Rust.

It turns out, unless you export a C API, or are willing to wade through mangled symbols, there's no real ability to do runtime modularity.

This is incredibly depressing and a sorry state of affairs.

It's also one that is, sadly, rather typical of modern languages.

The case for runtime modularity

Loading modules at runtime is incredibly useful. It lets us hotpatch functions, configure things and load functionality at runtime, and more.

IRC daemons do this as a standard practise since the dawn of time. Loading modules is integral to how a modern IRC daemon works.

Apache uses it to load modules at runtime. That way, you don't have to bloat your server with modules you'll never use, but they're always there if you need them, without recompiling. It's nifty!

So if something like this is so fundamental, why isn't it more widely supported?

The scourge of C

Because of name mangling, it's a complete pain to actually load things at runtime that aren't C symbols. C symbols aren't really mangled, per se (but see this and this), so loading them at runtime is fairly simple. As the article on name mangling says, we need mangling to actually express modern concepts such as monomorphisation and function overloading and such.

Unfortunately, pretty much no language makes it easy to mangle a symbol, especially at runtime. This is needed to use dlsym() or GetProcAddress() to load the symbol so we can use it.

Rust can't do it. C++ can't do it. D (to my knowledge) can't do it.

It's infuriating because the compiler knows how to do it. Yet it won't tell you how!!! Why, is beyond me.

The reason we have to do this name mangling crap to begin with is because of C. C hangs over all our heads as the lingua franca of all our systems. We must bend ourselves to its evil whims to accomplish runtime loading, because there is literally no other way.

Which means that we're all forced to speak C, or use horrible unportable hacks like reading the entire library's symbol table and demangling it.

Boost.DLL is one such package that does such cursed things. It can do a mangled import from a dynamic library by scanning the symbol table and demangling, at least on some platforms. Just don't look at how it's implemented. Unfortunately, such cursed things are not generally available for most languages, and if your compiler or OS isn't supported by Boost.DLL, too bad.

Why it matters

The fact anyone not using C has to either resort to cursed hacks or basically export all their symbols as C-style symbols with either bespoke mangling or no mangling means that we all wind up suffering with C's limitations.

It means we usually get no namespacing unless we do it ourselves. It means we can't hide our functions from C (which may be desirable, especially if we throw exceptions, because unwinding into C is a horrible idea). It means we have to contort ourselves into the C way of doing things, instead of expressing ourselves in our language's native tongue.

There's a spectre haunting programming. The spectre of C. I wish we could do away with it.

So... why do so few seem to care?

🤷‍♀️

I figured runtime modularity would be a killer feature everyone would want, especially a way to do it in a natural way for the language, instead of dropping to C or changing how our functions are exported.

But it seems not.

And it's saddening.

We should be able to do this! Something as simple as runtime modularity, which is far from a new concept, shouldn't be so hard or so hacky.

And what would fix this?

A better ABI where we can express our language's concepts would be helpful but since that won't ever happen...

Literally exporting a way to mangle a name from a path.

It isn't a perfect solution, but it would be a solution. It would be better than just trying to pretend we're speaking C.

I implore programming language authors, library authors, anyone, please please please, I beg you, export your name mangling APIs in an accessible way.

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay

Apparently changing your avatar is still not a supported thing here without editing the image manually on the filesystem. -_-

Grumble.

Well, whatever I guess, it is ActivityPub software, I should be grateful it works at all.

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay

I'll probably jot down my thoughts that are too long for my main account.

This thing also speaks ActivityPub apparently? So people on Fedi can also follow my blog. Super cool!

I will try to remember to use this for my big posts, and make a post back from Fedi to here when I make a new post.

Stay safe and healthy. 💜

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay