Avoid FnOnce, sending a Struct to Future as function argument

Hello,

I got a small program:


use std::{net::SocketAddr, vec, fs::File};

use futures::{future,};
use serde::{Serialize, Deserialize};
use serde_json::json;
use tokio::net::TcpListener;

use tokio_modbus::{
    prelude::{*},
    server::tcp::{accept_tcp_connection, Server}
};

#[derive(Serialize, Deserialize, Debug)]
struct Config{
    rtu_stub: String,
    ipv4: String,
    port: u16,
    default_slave: u8,
}

impl Config{
    fn new(rtu_stub:String, ipv4:String, port:u16, default_slave:u8) -> Config{
        Config{rtu_stub, ipv4, port, default_slave}
    }

    fn from_file(path:String) -> Config{
        let file = File::open(path).expect("can't read config file");
        serde_json::from_reader(file).unwrap_or_default()
    }
}

impl Default for Config{
    fn default() -> Self {
        Self { 
            rtu_stub: "/usr/bin/modbus/rtu_stub".to_string(),
            ipv4: "0.0.0.0".to_string(),
            port: 5502,
            default_slave: 1
         }
    }
}

struct ModbusService {}

impl tokio_modbus::server::Service for ModbusService {
    type Request = SlaveRequest;
    type Response = Response;
    type Error = std::io::Error;
    type Future = future::Ready<Result<Self::Response, Self::Error>>;

    fn call(&self, req: Self::Request) -> Self::Future {
        println!("{:?}", &req);
        
        let slave = req.slave;
        let req = req.request;

        let config = Config::from_file("config.json".to_string());
        
        match req {

            Request::ReadHoldingRegisters(addr, cnt) => {
                let json = json!({
                    "slave": slave,
                    "name": "read_holding_registers",
                    "args": [addr, cnt]
                });
                let output = std::process::Command::new("./rtu_stub")
                    .arg(format!("{}", json.to_string()))
                    .output()
                    .expect("can't start rtu_stub");
                if output.stderr.len() > 0 {
                    let err = std::str::from_utf8(&output.stderr).unwrap();
                    return future::ready(Err(std::io::Error::new(
                        std::io::ErrorKind::Other,
                        format!("error on rtu_stub: {}", err.trim())
                    )));
                }
                let value = serde_json::from_str(std::str::from_utf8(&output.stdout).unwrap()).unwrap();
                future::ready(Ok(Response::ReadHoldingRegisters(value)))
            },

            _ => {
                println!("SERVER: Exception::IllegalFunction - Unimplemented function code in request: {req:?}");
                future::ready(Err(std::io::Error::new(
                    std::io::ErrorKind::AddrNotAvailable,
                    "Unimplemented function code in request".to_string(),
                )))
            }
        }
    }
}


#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

    let config = Config::from_file("config.cfg".to_string());
    let socket_addr: SocketAddr = "0.0.0.0:5502".parse().unwrap();

    println!("Starting up server on {socket_addr}");

    let listener = TcpListener::bind(socket_addr).await.unwrap();
    let server = Server::new(listener);

    let on_connected = |stream, socket_addr| async move {
        let new_service = |_socket_addr| Ok(Some(ModbusService{}));
        accept_tcp_connection(stream, socket_addr, new_service)
    };

    let on_process_error = |err| {
        eprintln!("{err}");
    };

    let rt = tokio::runtime::Runtime::new().unwrap();

    rt.spawn( async move {
        server.serve(&on_connected, on_process_error).await.unwrap();
    });

    loop {}
}


I like to get rid of the second call of Config::from_file() in line 58.
I want to send it with ModbusService but it must be a Fn instead of FnOne (106 on_connect and 107 new_service).
How to do it right?

I may be misjudging the complexity of what you are trying to achieve, but isn't it possible to simply add config as a field to ModbusService? If you don't want to clone Config for every instance of ModbusService you can wrap it in an Arc. I was thinking something like this should work:

struct ModbusService {
    config: Arc<Config>,
}

impl ModbusService {
    fn new(config: Arc<Config>) -> Self {
        Self { config }
    }
}

and use the new function like this:

let config = Arc::new(Config::from_file("config.json".to_string()));

let on_connected = |stream, socket_addr| async move {
    let config = config.clone();
    let new_service = |_socket_addr| Ok(Some(ModbusService::new(config)));
    accept_tcp_connection(stream, socket_addr, new_service)
};

exactly what I am looking for but ...

what version of tokio-modbus are you using? It looks significantly different from v0.7.1 (latest version).

I use this:
'tokio-modbus = { git = "GitHub - slowtec/tokio-modbus: A tokio-based modbus library", default-features = false, features = ["tcp-server"] }'

because I used the example code from GitHub as template for my project.

Ok I think I got it now. Removing the async block should do the trick:

let on_connected = move |stream, socket_addr| {
    let config = config.clone();
    let new_service = |_socket_addr| Ok(Some(ModbusService::new(config)));
    future::ready(accept_tcp_connection(stream, socket_addr, new_service))
};

thanks a lot it works.

but what is different now, do I lose the ability to handle two connection simultaneously?

the programm eject one connection when I do a second one. I didn't test it before, so I am not sure if this edit the source of this behavior

yes, unfortunately I was right. When I try my code above again, it can handle more then one connection at time.

my new approach is a singleton with lazy_static to avoid a system call to read the file on every new request.

looks good for now

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.