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?
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();
}
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.