Fred Hors
Fred Hors

Reputation: 4136

What's the idiomatic way of consuming read-only struct's fields many times in the same function?

I'm just starting with Rust and since I'm a person who learns by doing I would like to fully understand the idiomatic way of solving cases like this in Rust.

Many replies I got in chat are all the same: "clone!".

I don't want to clone(), I'm here to learn better ways otherwise I'll go back to using Golang or Javascript.

Plus I'm afraid of cloning because I don't wanna allocate many times the same data (Config) in this case.

Even if I work with many workers (this is the case with actix-web) I wanna use references to that struct, not clone each time allocating many times the same data. Am I wrong?

In the simple below code as you can see I'm passing the struct Config by value to a function. In that function I need to refer to some fields of that struct and I'm havng serious troubles understanding how to do.

use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    let config = new_config();
    start(config).await.unwrap();
}

#[derive(Clone)]
pub struct Config { // never changes during the app life, it's a read only once created at runtime
    pub app_name: String,
    pub port: String,
}

fn new_config() -> Config {
    Config {
        app_name: String::from("APP_NAME"),
        port: String::from("1234"),
    }
}

pub struct AppState {
    pub config: Config,
}

async fn hello() -> impl Responder {
    HttpResponse::Ok()
}

async fn start(config: Config) -> std::io::Result<()> {
    println!("{}", config.app_name); //I'm using it the first time here

    let server = HttpServer::new(move || { // I'm moving it here
        App::new()
            .app_data(web::Data::new(AppState { config })) // here I get an error: cannot move out of `config`, a captured variable in an `Fn` closure. move occurs because `config` has type `Config`, which does not implement the `Copy` trait rustc E0507
            .route(&config.app_name, web::get().to(hello))
    });

    let addr = SocketAddr::from(([127, 0, 0, 1], config.port.parse::<u16>().unwrap())); // here another error: borrow of moved value: `config`. borrow occurs due to deref coercion to `str` rustc E0382

    server.bind(("127.0.0.1", 8080))?.run().await
}

Can you help me understand what's the idiomatic way of doing this with Rust?

Upvotes: 2

Views: 1086

Answers (1)

cdhowie
cdhowie

Reputation: 169143

In this case, since you can't guarantee that the Config value will live long enough, what you probably want is Arc<Config> which communicates thread-safe shared ownership of the value. You can clone the Arc which only duplicates the handle; there is still only one Config instance. For example:

pub struct AppState {
    pub config: Arc<Config>,
}

async fn start(config: Config) -> std::io::Result<()> {
    // Move the config into an Arc.
    let config = Arc::new(config);
    println!("{}", config.app_name);

    // Get a second Arc for the same Config; this second Arc will be moved
    // into the closure.
    let config2 = Arc::clone(&config);

    let server = HttpServer::new(move || {
        // We clone the handle to create the AppState because we can't move
        // out of a captured variable in an Fn closure.
        App::new()
            .app_data(web::Data::new(AppState { config: Arc::clone(&config2) }))
            .route(&config2.app_name, web::get().to(hello))
    });

    let addr = SocketAddr::from(([127, 0, 0, 1], config.port.parse::<u16>().unwrap()));

    server.bind(("127.0.0.1", 8080))?.run().await
}

Alternatively, you can leak a box, which gives you a 'static reference. This is suitable for data that will live for the entire duration of the program as long as it's not important that the data be cleanly dropped. (This is fine for String values; the OS will clean up the allocations when the program terminates.)

pub struct AppState {
    pub config: &'static Config,
}

async fn start(config: Config) -> std::io::Result<()> {
    // Move the config into a box, then leak it.
    let config = Box::leak(Box::new(config));
    println!("{}", config.app_name);

    let server = HttpServer::new(|| {
        App::new()
            .app_data(web::Data::new(AppState { config }))
            .route(&config.app_name, web::get().to(hello))
    });

    let addr = SocketAddr::from(([127, 0, 0, 1], config.port.parse::<u16>().unwrap()));

    server.bind(("127.0.0.1", 8080))?.run().await
}

The Arc approach is cleaner, while the leaked-box approach is easier to implement.

Upvotes: 5

Related Questions