Search code examples
rustcompiler-errorscompilationrust-cargorust-tokio

What is the idiomatic way in Rust of assigning values of different types to a variable, depending on the compilation target OS?


I'm working on a codebase which binds to a Tokio socket and manages a TCP connection. In production, it binds to an AF_VSOCK using the tokio-vsock crate.

While developing locally on Mac, the AF_VSOCK API isn't available as there is no hypervisor -> VM connection — it's just being run natively using cargo run.

When running locally, I have been creating a standard tokio::net::TcpListener struct and in production I have been creating a tokio_vsock::VsockListener. Both structs are mostly interchangeable and expose the same methods. The rest of the code works perfectly, regardless of which struct is being used.

So far, I have just kept both structs and simply commented out the one that isn't needed locally — this is clearly not "good practice". My code is below:

#[tokio::main]
async fn main() -> Result<(), ()> {
    // Production AF_VSOCK listener (comment out locally)
    let mut listener = tokio_vsock::VsockListener::bind(
        &SockAddr::Vsock(
          VsockAddr::new(
            VMADDR_CID_ANY,
            LISTEN_PORT,
          )
        )
    )
    .expect("Unable to bind AF_VSOCK listener");

    // Local TCP listener (comment out in production)
    let mut listener = tokio::net::TcpListener::bind(
        std::net::SocketAddr::new(
            std::net::IpAddr::V4(
                std::net::Ipv4Addr::new(0, 0, 0, 0)
            ),
            LISTEN_PORT as u16,
        )
    )
    .await
    .expect("Unable to bind TCP listener");

    // This works regardless of which listener is used
    let mut incoming = listener.incoming();

    while let Some(socket) = incoming.next().await {
        match socket {
            Ok(mut stream) => {
                // Do something
            }
        }
    }

    Ok(())
}

I tried using the cfg!() macro with target_os set as the condition, but the compiler complained that the types returned by both bind() methods were mismatched.

My question is: What is the idiomatic way in Rust of assigning different values with different types to a variable, depending on the compilation target OS?


Solution

  • There multiple options. The easiest and a very common one in regards to usage in the stdlib itself is using a #[cfg] macro (instead of cfg!(). The following code snippet clarifies it's usage:

    struct Linux;
    impl Linux {
        fn x(&self) -> Linux {        
            println!("Linux");
            Linux
        }
    }
    
    struct Windows;
    impl Windows {
        fn x(&self) -> Windows {
            println!("Windows");
            Windows
        }
    }
    
    fn main() {
        #[cfg(not(target_os = "linux"))]
        let obj = {
            let obj = Linux;
            obj
        };
        #[cfg(not(target_os = "windows"))]
        let obj = {
            let obj = Windows;
            obj
        };
        let _ = obj.x();
    }
    

    (see https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7088980d24c4a960c2158b091899d24d).

    In your case this would be (untested):

    #[tokio::main]
    async fn main() -> Result<(), ()> {
        #[cfg(target_os = "linux")]
        let mut listener = tokio_vsock::VsockListener::bind(
            &SockAddr::Vsock(
              VsockAddr::new(
                VMADDR_CID_ANY,
                LISTEN_PORT,
              )
            )
        )
        .expect("Unable to bind AF_VSOCK listener");
    
        #[cfg(target_os = "Mac")]
        let mut listener = tokio::net::TcpListener::bind(
            std::net::SocketAddr::new(
                std::net::IpAddr::V4(
                    std::net::Ipv4Addr::new(0, 0, 0, 0)
                ),
                LISTEN_PORT as u16,
            )
        )
        .await
        .expect("Unable to bind TCP listener");
    ...
    }
    

    Check https://doc.rust-lang.org/reference/conditional-compilation.html for available conditions, including feature flags, in case target_os is not applicable enough.

    The major difference between #[cfg] and cfg!() is that cfg! does not remove code. According to it's documentation: "cfg!, unlike #[cfg], does not remove any code and only evaluates to true or false". Due to that you get a compile error while using #[cfg] is more akin to if-defs in C/C++ and remove the unused code, hence the compiler never sees the type mismatch.