Style question: match () with guard vs if else if else if else if else

I just wrote the following code in my build script for a Lua crate:

let minor_version: &'static str = match () {
    () if cfg!(feature = "Lua-5.4") => "4",
    () if cfg!(feature = "Lua-5.3") => "3",
    () => panic!("Lua version not selected"),
};

Would you also agree that the above code is nicer than the following if/else approach?

let minor_version: &'static str = if cfg!(feature = "Lua-5.4") {
    "4"
} else if cfg!(feature = "Lua-5.3") {
    "3"
} else {
    panic!("Lua version not selected")
};

I wonder if match () with guards is a common idiom or considered bad style?

Note, both code snippets are formatted with rustfmt.

1 Like

I would consider it as an abuse of the match syntax, but then hey, its just my opinion.

4 Likes

I'd do

#[cfg!(feature = "Lua-5.4")]
const MINOR_VERSION: &'static str = "4";
#[cfg!(feature = "Lua-5.3")]
const MINOR_VERSION: &'static str = "3";
14 Likes

...with possibly also -

#[cfg(not(any(feature = "Lua-5.4", feature = "Lua-5.3")))]
compile_error!("Lua version not selected")
3 Likes

This question reminds me of the MultiWayIf language extension in GHC Haskell.

(Note that Haskell does write β€œ|” instead of β€œif” and also that it doesn’t enforce exhaustiveness in their β€œmatch”-equivalent.)

For some reason I always forget that I can use #[cfg(…)] inside a function too, and rustfmt even indents it:

fn main() {
    #[cfg(feature = "Lua-5.3")]
    const MINOR_VERSION: &'static str = "3";
    #[cfg(feature = "Lua-5.4")]
    const MINOR_VERSION: &'static str = "4";
}

(Playground)

1 Like

Yeah... I'd say that mutliway if / cond / when is one of the sorely missed syntactic trivialities in Rust.

Branching is very bug prone, and a clean syntax to express a general decision tree is a great reducer of accidental complexity.

2 Likes

Unlike the original code, your version doesn't treat the features as additive: If both Lua-5.4 and Lua-5.3 features are enabled, your version will produce a compile error while both of @jbe's versions will pick version 5.4 (which is presumably backwards-compatible with 5.3).

Yeah, that's intentional, and the reason why I used const rather than let

1 Like

Due to Cargo's feature unification, it's probably best to handle the both-specified case if that can be done reasonably, i.e. if Lua minor versions are actually backwards-compatible with each other. Otherwise, there's a risk of two library crates that work correctly in isolation triggering a compile error when they are used together in a single project.

Yeah, that's right, but actually I had planned to force the user of the crate to pick one version elsewhere in my code. It's good to know that constants can't shadow each other.

Well, Lua is not fully downwards compatible. That's why my crate will export one module v5_3 when built with Lua 5.3 and another module v5_4 when built with Lua 5.4.

Ideally, I would like to link with BOTH Lua versions at the same time (by somehow renaming symbols automatically?). But not sure how that could be done, if there was a way at all.

1 Like

Lua minor versions are notoriously incompatible with one another. See: Lua 5.4 Reference Manual

4 Likes

I haven't played around with linking C to Rust very much, but it might be possible to link Lua privately to your crate in such a way as to allow both Lua5_3 and Lua5_4 crates to be included in the same project (vs. modules in a single crate).

This should be possible, but not straightforward, with the traditional C build tools; I don't know whether or not Cargo is flexible enough to make that happen.

Another thing I wondered is whether I can make my build.rs script (see also reference on build scripts) detect which version is installed and compile the corresponding module if no particular Lua version has been requested. But maybe that's bad because it could lead to surprising results. Still wonder if it's technically possible to dynamically enable a feature based on the environment found (e.g. architecture found or other conditions at built-time).

Is there any way to figure out the path where libraries and header files are installed (ideally cross-platform)?

I'm asking because even if I require the user of the crate to specify the Lua version, I would like to check that the particular version is installed and where it is installed. Currently, the user of the crate must specify LUA_INCLUDE, LUA_LIB, and LUA_LIBNAME environment variables.

On FreeBSD, for example, I must set:

LUA_INCLUDE = /usr/local/include/lua53/
LUA_LIB = /usr/local/lib
LUA_LIBNAME = lua5.3

But for Ubuntu 20.4 LTS (which is used by the docs.rs build system), it should be:

LUA_INCLUDE = /usr/include/lua5.3
LUA_LIB = /usr/lib/x86_64-linux-gnu
LUA_LIBNAME = lua5.3

But how do I know I need to search for liblua5.3.a or liblua5.3.so (or liblua5.4.a or liblua5.4.so, respectively) in /usr/lib/x64_64-linux-gnu?

That's part of my build script as of now:

fn get_env(key: &str) -> Option<String> {
    /* … */
}

fn get_env_default<'a>(key: &'_ str, default: &'a str) -> std::borrow::Cow<'a, str> {
    /* … */
}

#[cfg(not(any(feature = "Lua-5.3", feature = "Lua-5.4")))]
compile_error!("must specify feature \"Lua-5.3\" or \"Lua-5.4\"");

fn main() {
    #[cfg(feature = "Lua-5.3")]
    const MINOR_VERSION: &'static str = "3";
    #[cfg(feature = "Lua-5.4")]
    const MINOR_VERSION: &'static str = "4";

    // get configuration from environment
    let lua_include = get_env("LUA_INCLUDE");
    let lua_lib = get_env("LUA_LIB");
    let lua_libname = get_env_default("LUA_LIBNAME", "lua");
    // how to search these automatically?

    /* … */
}

P.S.: Apparently the shell command pkg-config --cflags --libs lua-5.4 gives promising results both on Linux and FreeBSD at least. Not sure yet how to integrate that into my build script (or whether the cc crate offers a more "Rusty" interface, but I don't think it does).

P.P.S.: Maybe the pkg-config crate is my friend.


This got a bit off-topic, but in case someone is interested in the answer to my last question, I finally ended up doing this in my build.rs script:

use std::mem::take;
use std::process::{Command, Stdio};

#[cfg(not(any(feature = "Lua5_3", feature = "Lua5_4")))]
compile_error!("must specify feature \"Lua5_3\" or \"Lua5_4\"");

fn main() {
    let mut include_dirs: Vec<String> = Vec::new();
    let mut lib_dirs: Vec<String> = Vec::new();
    let mut lib_names: Vec<String> = Vec::new();

    // use `pkg-config` binary to determine Lua library name and location
    {
        #[cfg(feature = "Lua5_3")]
        const PKG_NAME: &'static str = "lua-5.3";
        #[cfg(feature = "Lua5_4")]
        const PKG_NAME: &'static str = "lua-5.4";
        #[cfg(not(any(feature = "Lua5_3", feature = "Lua5_4")))]
        const PKG_NAME: &'static str = unreachable!();
        let mut pkgcnf_cmd = Command::new("pkg-config");
        pkgcnf_cmd
            .args(["--cflags", "--libs", PKG_NAME])
            .stderr(Stdio::inherit());
        eprintln!("using pkg-config command: {pkgcnf_cmd:?}");
        let pkgcnf = pkgcnf_cmd.output().expect("could not execute pkg-config");
        eprintln!("pkg-config status: {:?}", pkgcnf.status);
        eprintln!(
            "pkg-config stdout: {:?}",
            String::from_utf8_lossy(&pkgcnf.stdout)
        );
        if !pkgcnf.status.success() {
            panic!("pkg-config returned with failure");
        }
        let mut parse_element = |s: String| {
            if !s.is_empty() {
                if s.len() >= 2 {
                    let prefix = &s[0..2];
                    let value = &s[2..];
                    match prefix {
                        "-I" => include_dirs.push(value.to_string()),
                        "-L" => lib_dirs.push(value.to_string()),
                        "-l" => lib_names.push(value.to_string()),
                        _ => (),
                    }
                }
            }
        };
        let mut element: String = Default::default();
        let mut escape: bool = false;
        for ch in String::from_utf8(pkgcnf.stdout)
            .expect("invalid UTF-8 from pkg-config")
            .chars()
        {
            if escape {
                element.push(ch);
                escape = false;
            } else if ch == '\\' {
                escape = true;
            } else if ch.is_ascii_whitespace() {
                parse_element(take(&mut element));
            } else {
                element.push(ch);
            }
        }
        if escape {
            panic!("unexpected EOF from pkg-config (escape character at end)");
        }
        parse_element(element);
        if lib_names.is_empty() {
            panic!("pkg-config did not return any library name");
        }
    }

    // create automatic bindings
    {
        let mut builder = bindgen::Builder::default();
        for dir in &include_dirs {
            builder = builder.clang_arg(format!("-I{dir}"));
        }
        builder = builder.header("src/cmach.c");
        builder = builder.parse_callbacks(Box::new(bindgen::CargoCallbacks));
        let bindings = builder.generate().expect("unable to generate bindings");
        let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
        bindings
            .write_to_file(out_path.join("ffi_cmach.rs"))
            .expect("unable to write bindings");
    }

    // build own C lib
    {
        println!("cargo:rerun-if-changed=src/cmach.c");
        let mut config = cc::Build::new();
        for dir in &include_dirs {
            config.include(dir);
        }
        config.file("src/cmach.c");
        config.compile("libffi_cmach.a");
    }

    // link with Lua
    for dir in &lib_dirs {
        println!("cargo:rustc-link-search=native={}", dir);
    }
    for name in &lib_names {
        println!("cargo:rustc-link-lib={}", name);
    }
}

Maybe it's not very elegant, but it will throw a nice error when no Lua version is selected and disallow both versions to be selected. Moreover, the correct include and library directories and library name is determined automatically.

After some struggles with (and headaches about) the crates.io/docs.rs/cargo workflow, I was also able to publish everything as a crate: sandkiste_lua.


Regarding linking with different versions of the same C library at the same time, I opened a new thread here: