How can I override a constant via a compiler option? - rust

Is it possible to define a constant in source code that can be overridden by a compiler flag? That is, something like setting a #define value in the C preprocessor with the -D key=val option to the compiler.
I have read about conditional compilation via the #[cfg(...)] attribute, but that only seems to support booleans. I want to allow the user to set the value of a constant during compilation.
Something like this:
#[from_cfg("max_dimensions")]
const MAX_DIMENSIONS: usize = 100_000;

Building on Lukas Kalbertodt's answer, you can get the environment variable as a constant number with some extra indirection, namely by using a build script.
build.rs
use std::{env, fs::File, io::Write, path::Path};
fn main() {
let out_dir = env::var("OUT_DIR").expect("No out dir");
let dest_path = Path::new(&out_dir).join("constants.rs");
let mut f = File::create(&dest_path).expect("Could not create file");
let max_dimensions = option_env!("MAX_DIMENSIONS");
let max_dimensions = max_dimensions
.map_or(Ok(10_000), str::parse)
.expect("Could not parse MAX_DIMENSIONS");
write!(&mut f, "const MAX_DIMENSIONS: usize = {};", max_dimensions)
.expect("Could not write file");
println!("cargo:rerun-if-env-changed=MAX_DIMENSIONS");
}
main.rs
include!(concat!(env!("OUT_DIR"), "/constants.rs"));
fn main() {
println!("The value is {} ({})", MAX_DIMENSIONS, MAX_DIMENSIONS + 1);
}
$ cargo run
The value is 10000 (10001)
$ MAX_DIMENSIONS=17 cargo run
The value is 17 (18)
$ MAX_DIMENSIONS=1 cargo run
The value is 1 (2)

No, you can't define constants (read: const bindings) with a compiler flag. But you can use the env! macro for something similar. It reads some environment variable at compile time.
const MAX_DIMENSIONS_RAW: &'static str = env!("MAX_DIMENSIONS");
Sadly, this returns a string and not an integer. Furthermore we can't yet call arbitrary functions (like parse) at compile time to calculate a constant. You could use lazy_static to achieve something similar:
lazy_static! {
static ref MAX_DIMENSIONS: usize = MAX_DIMENSIONS_RAW.parse().unwrap();
}
Of course you should add proper error handling. If your user doesn't need to define the environment variable, you can use option_env!.
With this approach, you can pass the setting at build time:
$ MAX_DIMENSIONS=1000 cargo build

Related

How to build a Rust library depended on a variable passed through command line? [duplicate]

Is it possible to define a constant in source code that can be overridden by a compiler flag? That is, something like setting a #define value in the C preprocessor with the -D key=val option to the compiler.
I have read about conditional compilation via the #[cfg(...)] attribute, but that only seems to support booleans. I want to allow the user to set the value of a constant during compilation.
Something like this:
#[from_cfg("max_dimensions")]
const MAX_DIMENSIONS: usize = 100_000;
Building on Lukas Kalbertodt's answer, you can get the environment variable as a constant number with some extra indirection, namely by using a build script.
build.rs
use std::{env, fs::File, io::Write, path::Path};
fn main() {
let out_dir = env::var("OUT_DIR").expect("No out dir");
let dest_path = Path::new(&out_dir).join("constants.rs");
let mut f = File::create(&dest_path).expect("Could not create file");
let max_dimensions = option_env!("MAX_DIMENSIONS");
let max_dimensions = max_dimensions
.map_or(Ok(10_000), str::parse)
.expect("Could not parse MAX_DIMENSIONS");
write!(&mut f, "const MAX_DIMENSIONS: usize = {};", max_dimensions)
.expect("Could not write file");
println!("cargo:rerun-if-env-changed=MAX_DIMENSIONS");
}
main.rs
include!(concat!(env!("OUT_DIR"), "/constants.rs"));
fn main() {
println!("The value is {} ({})", MAX_DIMENSIONS, MAX_DIMENSIONS + 1);
}
$ cargo run
The value is 10000 (10001)
$ MAX_DIMENSIONS=17 cargo run
The value is 17 (18)
$ MAX_DIMENSIONS=1 cargo run
The value is 1 (2)
No, you can't define constants (read: const bindings) with a compiler flag. But you can use the env! macro for something similar. It reads some environment variable at compile time.
const MAX_DIMENSIONS_RAW: &'static str = env!("MAX_DIMENSIONS");
Sadly, this returns a string and not an integer. Furthermore we can't yet call arbitrary functions (like parse) at compile time to calculate a constant. You could use lazy_static to achieve something similar:
lazy_static! {
static ref MAX_DIMENSIONS: usize = MAX_DIMENSIONS_RAW.parse().unwrap();
}
Of course you should add proper error handling. If your user doesn't need to define the environment variable, you can use option_env!.
With this approach, you can pass the setting at build time:
$ MAX_DIMENSIONS=1000 cargo build

Can I create my own conditional compilation attributes?

There are several ways of doing something in my crate, some result in fast execution, some in low binary size, some have other advantages, so I provide the user interfaces to all of them. Unused functions will be optimized away by the compiler. Internal functions in my crate have to use these interfaces as well, and I would like them to respect the user choice at compile time.
There are conditional compilation attributes like target_os, which store a value like linux or windows. How can I create such an attribute, for example prefer_method, so I and the user can use it somewhat like in the following code snippets?
My crate:
#[cfg(not(any(
not(prefer_method),
prefer_method = "fast",
prefer_method = "small"
)))]
compile_error("invalid `prefer_method` value");
pub fn bla() {
#[cfg(prefer_method = "fast")]
foo_fast();
#[cfg(prefer_method = "small")]
foo_small();
#[cfg(not(prefer_method))]
foo_default();
}
pub fn foo_fast() {
// Fast execution.
}
pub fn foo_small() {
// Small binary file.
}
pub fn foo_default() {
// Medium size, medium fast.
}
The user crate:
#[prefer_method = "small"]
extern crate my_crate;
fn f() {
// Uses the `foo_small` function, the other `foo_*` functions will not end up in the binary.
my_crate::bla();
// But the user can also call any function, which of course will also end up in the binary.
my_crate::foo_default();
}
I know there are --cfg attributes, but AFAIK these only represent boolean flags, not enumeration values, which allow setting multiple flags when only one enumeration value is valid.
Firstly, the --cfg flag supports key-value pairs using the syntax --cfg 'prefer_method="fast"'. This will allow you to write code like:
#[cfg(prefer_method = "fast")]
fn foo_fast() { }
You can also set these cfg options from a build script. For example:
// build.rs
fn main() {
println!("cargo:rustc-cfg=prefer_method=\"method_a\"");
}
// src/main.rs
#[cfg(prefer_method = "method_a")]
fn main() {
println!("It's A");
}
#[cfg(prefer_method = "method_b")]
fn main() {
println!("It's B");
}
#[cfg(not(any(prefer_method = "method_a", prefer_method = "method_b")))]
fn main() {
println!("No preferred method");
}
The above code will result in an executable that prints "It's A".
There's no syntax like the one you suggest to specify cfg settings. The best thing to expose these options to your crates' users is through Cargo features.
For example:
# Library Cargo.toml
# ...
[features]
method_a = []
method_b = []
// build.rs
fn main() {
// prefer method A if both method A and B are selected
if cfg!(feature = "method_a") {
println!("cargo:rustc-cfg=prefer_method=\"method_a\"");
} else if cfg!(feature = "method_b") {
println!("cargo:rustc-cfg=prefer_method=\"method_b\"");
}
}
# User Cargo.toml
# ...
[dependencies.my_crate]
version = "..."
features = ["method_a"]
However, in this case, I'd recommend just using the Cargo features directly in your code (i.e. #[cfg(feature = "fast")]) rather than adding the build script since there's a one-to-one correspondence between the cargo feature and the rustc-cfg being added.

Can Rust cargo features contain a value? [duplicate]

Is it possible to define a constant in source code that can be overridden by a compiler flag? That is, something like setting a #define value in the C preprocessor with the -D key=val option to the compiler.
I have read about conditional compilation via the #[cfg(...)] attribute, but that only seems to support booleans. I want to allow the user to set the value of a constant during compilation.
Something like this:
#[from_cfg("max_dimensions")]
const MAX_DIMENSIONS: usize = 100_000;
Building on Lukas Kalbertodt's answer, you can get the environment variable as a constant number with some extra indirection, namely by using a build script.
build.rs
use std::{env, fs::File, io::Write, path::Path};
fn main() {
let out_dir = env::var("OUT_DIR").expect("No out dir");
let dest_path = Path::new(&out_dir).join("constants.rs");
let mut f = File::create(&dest_path).expect("Could not create file");
let max_dimensions = option_env!("MAX_DIMENSIONS");
let max_dimensions = max_dimensions
.map_or(Ok(10_000), str::parse)
.expect("Could not parse MAX_DIMENSIONS");
write!(&mut f, "const MAX_DIMENSIONS: usize = {};", max_dimensions)
.expect("Could not write file");
println!("cargo:rerun-if-env-changed=MAX_DIMENSIONS");
}
main.rs
include!(concat!(env!("OUT_DIR"), "/constants.rs"));
fn main() {
println!("The value is {} ({})", MAX_DIMENSIONS, MAX_DIMENSIONS + 1);
}
$ cargo run
The value is 10000 (10001)
$ MAX_DIMENSIONS=17 cargo run
The value is 17 (18)
$ MAX_DIMENSIONS=1 cargo run
The value is 1 (2)
No, you can't define constants (read: const bindings) with a compiler flag. But you can use the env! macro for something similar. It reads some environment variable at compile time.
const MAX_DIMENSIONS_RAW: &'static str = env!("MAX_DIMENSIONS");
Sadly, this returns a string and not an integer. Furthermore we can't yet call arbitrary functions (like parse) at compile time to calculate a constant. You could use lazy_static to achieve something similar:
lazy_static! {
static ref MAX_DIMENSIONS: usize = MAX_DIMENSIONS_RAW.parse().unwrap();
}
Of course you should add proper error handling. If your user doesn't need to define the environment variable, you can use option_env!.
With this approach, you can pass the setting at build time:
$ MAX_DIMENSIONS=1000 cargo build

How to set cfg options to compile conditionally?

I am working on some code where a buffer is backed by a statically sized array. Since Rust and the build tools provided by it offer the possibilities to compile conditionally, I can do something like this:
struct Buffer {
// default case, if none is set
#[cfg(not(buffersize))]
buffer: [f32; 16],
#[cfg(buffersize = "32")]
buffer: [f32; 32],
#[cfg(buffersize = "64")]
buffer: [f32; 64],
}
impl Buffer {
fn new() -> Buffer {
Buffer {
#[cfg(not(buffersize))]
buffer: [0.0; 16],
#[cfg(buffersize = "32")]
buffer: [0.0; 32],
#[cfg(buffersize = "64")]
buffer: [0.0; 64],
}
}
}
There is another question that uses features to compile the code conditionally. Using features alone, I would have to combine buffersize and the actual value e.g. buffersize16. Is it possible to provide the cfg flags to Cargo, or would I need to provide them directly to rustc?
You can set the environnment variable RUSTFLAGS or set rustflags variable in .cargo/config.
From environment-variables
RUSTFLAGS — A space-separated list of custom flags to pass to all
compiler invocations that Cargo performs. In contrast with cargo
rustc, this is useful for passing a flag to all compiler instances.
In your example, you could use :
RUSTFLAGS='--cfg buffersize="32"' cargo build
I want to post an update to my question as an additional option on how to pass (numeric) configuration values at compile time, that's possible through a build script.
Suppose you have following build script inside your project:
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
fn main() {
println!("cargo:rerun-if-env-changed=SIZE");
let out_dir = env::var("OUT_DIR").unwrap();
let dest = Path::new(&out_dir).join("consts.rs");
let mut out_file = File::create(&dest).expect("Cannot create file");
let size: usize = env!("SIZE").parse().unwrap();
write!(&out_file, "pub const S : usize = {};", size);
}
It reads an environment variable at compile time, parses it as usize and writes a rust file (consts.rs) containing only the constant. Now, in your application code you can include this file and use the constant to eg. allocate memory on the stack:
include!(concat!(env!("OUT_DIR"), "/consts.rs"));
fn main() {
let array = [0.0f32; S];
println!("array len= {:?}", array.len());
}
The downside of this trick is, that you have to recompile the whole project (or parts of it) whenever the value of the environment variable changes, since cargo:rerun-if-env-changed=SIZE doesn't get captured. It also implies to always keep knowledge of this configuration option, but this could be wrapped in an additional build script like a makefile. Even if this is not the most elegant path to choose, it might be an option on certain occasions.
It would be nice to have this as macro option, though.

Is it possible to declare config tokens within a source file?

In Rust, it's possible to perform conditional compilation as follows.
#[cfg(rust_version = "1.10")]
fn my_func() {}
Is it possible to define variables for cfg to check within the same source file?
For example:
// leave off, just a quick test to enable when troubleshooting.
#define use_counter 1 // C style (not valid Rust)
#[cfg(use_counter == "1")]
static mut fn_counter: usize = 0;
fn my_func() {
#[cfg(use_counter = "1")]
unsafe { fn_counter += 1; }
}
main () {
// code calling 'my_func'
// print how many times the function is called.
#[cfg(use_counter = "1")]
unsafe { println!("Function count {}", fn_counter); }
}
I'm not asking how to write a function counter, it's just an example of optionally inserting logic into a source file.
Yes, this is written as #[cfg(use_counter)]. Such flags can be enabled or disabled on the command line at compile time and are not exposed in Cargo.toml.
fn main() {
#[cfg(use_counter)]
println!("counter is enabled");
}
Using Cargo, run with the feature disabled:
$ cargo run
Using Cargo, run with the feature enabled:
$ RUSTFLAGS="--cfg use_counter" cargo run
Compile directly with the feature disabled:
$ rustc src/main.rs
Compile with the feature enabled:
$ rustc src/main.rs --cfg use_counter

Resources