Application Architecture in Rust

New to Rust. Most of my experience in programming has been in Object Oriented languages. While I understand Object Oriented is possible to do in Rust, I find it quickly becomes painful and misses a lot of the guarantees that Rust can provide. Specifically, an OO app consists of a graph of objects. These objects send messages to each other and doing so, mutate each other.

Trying this approach quickly took me to rather advanced(?) features in Rust like interior mutability. The worst is that it prevents the compiler from verifying ownership.

To me that means that while OO is possible in Rust, it is not the intended way.

So my question is: what is the paradigm that naturally fits Rust? Or, how should I approach modeling my applications in Rust?

Can you describe in general terms the kind of applications you're writing? Command line, GUI, client/server, video games, etc.

1 Like

Generally Rust prefers it when your graph of objects form a tree, preferably with minimal amounts of subclassing-like constructs.

It is generally also perfectly okay with directed acyclic graphs as long as anything with multiple owners is immutable.

4 Likes

Good question. I'm thinking about a server application with some downstream dependencies.

For a server application, I find that most of the stuff in each request is owned exclusively by that request, and these parts are usually able to form a nice tree, making Rust happy.

Then you probably have a few pieces of shared mutable state. E.g. if you were implemented redis, you might have a shared mutable hashmap of some sort. I find that the contents of this shared mutable state can usually be structured in a nice tree, making Rust happy, however you are going to need an Arc + Mutex combination somewhere at the top of this tree to share it between all the request handlers.

For a larger example of this, you can check out mini-redis.

3 Likes

If you have found a way to prevent the compiler from verifying ownership, without using "unsafe" you should report that as a bug.

I am referring to this from the docs about RefCell and Interior Mutability:

To mutate data, the pattern uses unsafe code inside a data structure to bend Rust’s usual rules that govern mutation and borrowing.

In my view if one is coming from the abstract world of OOP and writing application code in Rust one should not be using "unsafe" anywhere.

Save that for the down'n'dirty library code. I have yet to ever need "unsafe".

The OP was referring to ownership rules being checked at runtime when using interior mutability, which is correct. It doesn't require the use of unsafe (to use).

3 Likes

As @alice mentioned, Rust likes you to structure your application so that there is a very clear ownership story. The graph-of-objects approach doesn't really line up with this because you have shared mutation with no clear "owner" in charge of (for example) destroying a resource, so while it is definitely possible to write code in this way, you'll encounter a bit of friction.

The borrow checker and Rust's immutable-by-default nature make it much more amenable to functional programming. You also tend to use the type system a lot more for expressing concepts and indicating what is or isn't possible, this pushes a lot of runtime checks (e.g. imagine a User holding a boolean flag to indicate whether they are logged in) to compile time (e.g. it's just not possible for a logged out user to change their password because set_password() isn't a method on LoggedOutUser).

What I tend to do is use OO concepts for the high level stuff like encapsulating behaviour and state within an "object" who's methods you call to do things. Then for the implementation I'll take a more functional approach, making use of composition and phrasing the task as a series of immutable transformations instead of state mutation/manipulation.

1 Like

Sounds interesting. Do you have a repo I can peek in to make sure I understand?

I understand. But which "architecture" or "pattern" do you use to model an application this way?

I find that junior developers (in this case with Rust - myself included) benefit a lot from having a "standard" way to approach problems - in this case, modelling an application. True, that "standard" way won't fit all cases, but it can cover enough cases and at least provide developers with a place to start.

For OOP I tend to offer junior developers to use something like this. Specifically, to find the following parts of their application:

  • UI - for example a CLI, or an HTTP API.
  • Core - the "business logic". Contains "services" that encode application logic and interfaces ("ports") to 3rd party systems (e.g. DB, file system, other service).
  • Adapters - the concrete implementations of the "ports" to other systems.

In my experience this mostly yields applications that are easy to maintain and test. Mostly because of the direction of dependencies (UI -> Core; Adapters -> Core).

However, this also quickly yields a graph of objects with multiple ownership: for example, both a CLI class and an HTTP resource class need to use and mutate an application service (in the "Core" layer). In Rust this seems needlessly painful.

Hence the question -- I'm looking for a "beginner friendly" "covers 80% of the cases" "template" for how to structure an application in a way that makes Rust happy.

1 Like

We have to be careful with the words "ownership" and "owner" in Rust world.

In Rust "ownership" has a specific meaning. The owner of a piece of data is the one responsible for destroying (deallocate/delete/free) the data when it is no longer required. There can only be one owner. Ownership can me "moved" to some other owner but there is always only one. This must be so otherwise we would end up with multiple users of data thinking they should free it, or none. Confusion would reign as it often does in languages like C, C++. Resulting in multiple freeing the same data, use of freed data or memory leaks.

Everyone else is a "borrower", accessing the data through mutable or immutable references.

In the world of interpreted languages none of the code you write "owns" the data in that way, the interpreter does. The interpreter will decide to destroy things when it notices there are no more users of the data.

What one has with interpreted languages is multiple references to data.The same effect can be achieved in Rust by using smart pointers, like Rc or Arc. Dictionaries and arrays in such languages map to Vectors, HasMaps and other containers in Rust.

My impression is that the only thing Rust is lacking (at least with any convenience) from Object Oriented Programming is inheritance. However those into OOP now a days chant "Favour composition of inheritance". A notion that comes from the famous "Gang of Four" book on patterns. Inheritance is increasingly seen as an anti-pattern by many. So Rust is not missing anything of importance there.

I find it hard to think of that "standard" architecture you speak of. Something that is applicable to the widest range of tasks. There is so much variability in tasks.

Your mention of "adapters" immediately brings to mind Rust's traits. Traits can be thought of as interfaces. There can be many implementations of an interface. They an be swapped in and out of the "core" logic as required.

Your mention of "dependencies (UI -> Core; Adapters -> Core)." brings to mind my favourite way to think about problems. A collection of black boxes, each dedicated to a single , well defined, task. Communicating to each other via channels. std::sync::mpsc - Rust

Which is how my first production code in Rust has been put together. When using an async run time like Tokio and it's channel mechanisms this does not even consume expensive real threads.

1 Like

Just because the two other use the core layer doesn't mean this has to get you i to trouble. Instead of making the core layer some sort of object you're sharing between all things that need to access it, make a new object for each thing that is accessing the core.

Most web applications are going to be storing the shared data in a database rather than in memory as fields in that core object anyway, so you usually don't need to share it.

In some sense it would be similar to what you would do in php, where each request handler is entirely isolated from the others, so they also need to create their own object each time

I think you should try to read the source of mini-redis, which I linked earlier.

Only because your previous experience (i.e. other languages) promoted a graph of objects with shared mutation. The layering you outlined works quite well in Rust, you just have to think about the ownership story a bit more or you may need to avoid patterns you used to use.

You often find that Rust's ownership system actually helps you enforce that direction of dependencies and punishes the sorts of complicated code or incidental complexity that junior developers are prone to writing.

Hmm... The project I had in mind when talking about that is currently closed-source, but one example is a CAD engine I've been working on.

Take the rendering system. In the WebAssembly bindings (the interface used by JavaScript when running in the browser) the entrypoint to the rendering system is the Screen type which exposes high-level methods like pan() and render().

Internally, when you call render() it will call the arcs_render::passes::render() function. Inside render() we run each pass in turn, where a "pass" is just some function which paints a particular aspect of the screen. Rendering tends to be a very procedural process (you are literally mutating a canvas) so there isn't much functional programming going on, but that's the best example I can come up with off the top of my head.

The closed-source project I'm talking about has a component which is analogous to a parser and compiler for a programming language. So you can imagine how the application could be phrased as a bunch of AST transformations.

Right. And I guess for stateful objects I actually need to think about how other objects access it -- which is what Rust is trying to tell me :slight_smile:

I think you should try to read the source of mini-redis, which I linked earlier.

I did. But it has an ad-hoc structure (which is totally fine given its size). Or maybe I just failed to see a structure I recognise when looking at it.

Sure. The reason I keep mentioning mini-redis is that it provides an example of how you can answer that question. Another possible answer is the actor pattern.

1 Like