We're hiring!
*

Persian Rug - It really ties the Rust room together

Edmund Smith avatar

Edmund Smith
August 09, 2023

Share this post:

Reading time:

Rust is a modern language known for its memory safety, efficiency, and wide range of high-level features. But many beginners also run into something else in Rust: how surprisingly difficult it is to represent some common designs. The internal sense we all have of what kinds of things will lead to difficult code doesn't work well at all in Rust for most newcomers, especially when that sense has been trained by other languages, like C++.

In this series of blog posts, we'll go through a Rust crate which can simplify the creation and management of interconnected objects. It's not a complete solution, and we'll discuss its limitations in detail in future parts, as well as what it might take to deal with these problems more comprehensively.

An example

An object soup


Let's think about a toy problem, so simple it may not even seem worth discussing at first. We have two types of objects, users and groups, and some links between them. Let's say we're going to build up a store of them in memory. Maybe it's part of a slightly beefed-up authentication system we're writing.

In C++, we might start with something like this:

class group;
class user {
    std::vector<std::shared_ptr> groups;
};
class group {
    std::optional<std::shared_ptr> leader;
};

Things are not completely straightforward even here, and we'll probably end up with weak pointers in the mix somewhere in the final design, but it's a starting point that we can build code upon.

There's nothing directly analogous to this in Rust, for reasons we'll explore later. We might try something like this:

struct User {
    groups: Vec<Arc>,
}
struct Group {
    leader: Option<Arc>,
}

Immediately, we will run into problems. An Arc does not provide mutable access to its contents once there are multiple pointers to the same data.

Suppose we have one user, u, and one group g, where u is the leader of g. We might try:

let g = Arc::new(Group { leader: None });
let u = Arc::new(User { groups: vec![g.clone()] });

At this point we're stuck, we can't edit g to make u the leader. If we flip things around, it's not any better:

let u = Arc::new(User { groups: Vec::new() });
let g = Arc::new(Group { leader: Some(u.clone()) });

Now we can't go back and edit u to mark them as a member of g. One way or another our representation is incomplete.

Rust aficionados might note that if we were really careful with ordering, and also avoided the clone, then we could muddle through this example using get_mut on Arc, which only works when a single copy of the Arc exists. But this approach would become increasingly impractical as we add more users and groups, or if the model were to become more complex.

We'll explore other approaches in later parts of this series, but none of them safely solves this basic case satisfactorily. We'll talk about why that is in detail - why this is fundamentally hard in Rust - in the next part. In short, it's because Rust's safety comes in part from controlling data access patterns and an object soup makes that very difficult. But to skip to the subject of this series: here's a viable alternative using persian-rug:

#[contextual(Rug)]
struct User {
    groups: Vec<Proxy>,
}
#[contextual(Rug)]
struct Group {
    leader: Option<Proxy>,
}
#[derive(Default)]
#[persian_rug]
struct Rug(#[table] User, #[table] Group);

So what is all of this? And what's this Rug? The basic idea of persian-rug is to control access to the whole soup with a single object. To do this, the single object - the rug - owns all the memory, and we refer to objects with lightweight links called proxies. You can think of a proxy as a handle which requires a reference to the rug in order to obtain a reference to the object. The macros you see in the above, like contextual and persian_rug, just add the supporting code you need to make this all work conveniently, without too much additional noise.

The above definitions give us a collection of interlinked objects which are fully mutable, and can be built conveniently. For example, here's a working version of the code we attempted with Arc using them:

let r: Rug = Default::default();
let u = r.add(User { groups: Vec::new() });
let g = r.add(Group { leader: Some(u) });
r.get_mut(&u).groups.push(g);

Compared to our earlier attempt, this version has no additional construction order dependencies. By holding all the data in the rug, we're allowing Rust to treat the entire collection of objects as a thing to which access can be granted. That means we sidestep the problem of individual objects participating in a web of direct references to others. Rust's normal safety rules can be based on examining references to the rug, and most safety checking can happen at compile time.

Next

In the following parts, we'll start by going into the details of why these soups are difficult to manage in Rust, and the built-in solutions you might try without persian-rug. Then we'll look at the details of how persian-rug works, and finally finish by looking at the limitations currently present, compared with other third-party solutions, and how those limitations might one day be improved.

Comments (0)


Add a Comment






Allowed tags: <b><i><br>Add a new comment:


Search the newsroom

Latest Blog Posts

WhisperSpeech: Exploring New Horizons in Text-to-Speech Technology

13/09/2023

Text-to-speech (TTS) models are playing a transformative role, from enriching audiobooks to enhancing podcasts and even improving interactions…

Bridging IIO and Input in Linux

21/08/2023

In Linux, the Industrial Input/Output subsystem manages devices like Analog to Digital Converters, Light sensors, accelerometers, etc. On…

Pushing testing laboratory performance limits by benchmarking LAVA - Part 1

10/08/2023

Collabora's main testing laboratory has grown to automate testing on over 150 devices of about 30 different types. The lab receives job…

Persian Rug - It really ties the Rust room together

09/08/2023

Rust is a modern language known for its memory safety, efficiency, and wide range of high-level features. But many beginners also run into…

Triple Threat: The Power of Transcription, Summary, and Translation

03/08/2023

At Collabora, we're committed to bringing people together. That's why we're pushing state-of-the-art machine-learning techniques like Large…

Booting on Radxa's Rock-5B without any media used

31/07/2023

I have been working on getting U-boot upstream up to speed for the Radxa Rock-5B Rockchip RK3588 board. One of the cool features that I…

Open Since 2005 logo

We use cookies on this website to ensure that you get the best experience. By continuing to use this website you are consenting to the use of these cookies. To find out more please follow this link.

Collabora Ltd © 2005-2023. All rights reserved. Privacy Notice. Sitemap.