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

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

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 can I compile a string supplied on the command line during compilation into my Rust binary?

I want to make it so that when my program starts it prints to stderr:
This is Program X v. 0.1.0 compiled on 20180110. Now listening on stdin,
quit with SIGINT (^C). EOF is ignored. For licensing information, read
LICENSE. To suppress this message, supply --quiet or --suppress-greeting
In C/C++, I would achieve this with a Makefile, e.g.:
VERSION = 0.1.0
FLAGS = -Wall -pipe -O3 -funroll-loops -Wall -DVERSION="\"$(VERSION)\"" -DCOMPILED_AT="\"`date +%Y%m%d`\""
Then, in the source code, I would use those constants as I pleased, perhaps in a call to fprintf. After checking if they actually existed with #ifdef, of course.
How can this be achieved in Rust? Do I need to use a procedural macro? Can I use cargo somehow?
I know that env!("CARGO_PKG_VERSION") can be used as a replacement for VERSION, but what about COMPILED_AT?
There are two ways to do this.
Use a build.rs script
The benefit of using a build.rs script is that other users compiling your program will not have to invoke cargo in a special way or set up their environment. Here is a minimal example of how to do that.
build.rs
use std::process::{Command, exit};
use std::str;
static CARGOENV: &str = "cargo:rustc-env=";
fn main() {
let time_c = Command::new("date").args(&["+%Y%m%d"]).output();
match time_c {
Ok(t) => {
let time;
unsafe {
time = str::from_utf8_unchecked( &t.stdout );
}
println!("{}COMPILED_AT={}", CARGOENV, time);
}
Err(_) => exit(1)
}
}
src/main.rs
fn main() {
println!("This is Example Program {} compiled at {}", env!("CARGO_PKG_VERSION"), env!("COMPILED_AT"));
}
Cargo.toml
[package]
name = "compiled_at"
version = "0.1.0"
authors = ["Fredrick Brennan <copypaste#kittens.ph>"]
build = "build.rs"
[dependencies]
Obviously this can be tweaked to get it to work on other platforms which don't have a /bin/date or to compile in other things such as the Git version number. This script was based on the example provided by Jmb, which shows how to add Mercurial information into your program at compile time.
This guarantees that COMPILED_AT will either be set or the build will fail. This way other Rust programmers can build just by doing cargo build.
[osboxes#osboxes compiled_at]$ cargo run
Compiling compiled_at v0.1.0 (file:///home/osboxes/Workspace/rust/compiled_at)
Finished dev [unoptimized + debuginfo] target(s) in 1.27 secs
Running `target/debug/compiled_at`
This is Example Program 0.1.0 compiled at 20180202
Use env!(), then require users to set their environment prior to building
It occurred to me after I asked this question to try the following, and it does work (vim is in use, thus the escaped %s), cargo does pass my environment down to rustc:
COMPILED_AT=`date +\%Y\%m\%d` cargo run
Then, in Rust:
fn main() {
eprintln!("{}", env!("COMPILED_AT"));
}
The Rust compiler refuses to compile the code if I don't supply the environment variable, which is a nice touch.
error: environment variable `COMPILED_AT` not defined
--> src/main.rs:147:21
|
147 | eprintln!("{}", env!("COMPILED_AT"));
| ^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
This way is extremely hacky and is guaranteed to annoy other Rust programmers who just expect to build with cargo build, but it does work. If you can, it is recommended to use the build.rs instead.
You can use the build.rs script to add an environment variable to the cargo environment or to create an extra source file that contains the information you need. That way you can add a lot of information about the build environment. Here is a full example that creates a build_info.rs source file containing:
Version information built from the Mercurial tag, revision hash and status (including the date if the source folder is different from the Mercurial folder).
Toolchain information, including the compiler version and the build profile (eg. debug or release) thanks to the rustc_version crate.
Plus it extracts some information from cargo (like the package name) so it doesn't have to be duplicated between the Cargo.toml and the source code).
#[macro_use] extern crate map_for;
extern crate rustc_version;
extern crate time;
use rustc_version::{ Channel, version_meta };
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::process::Command;
/// Run mercurial with the given arguments and return the output.
fn run_hg<S: AsRef<OsStr>> (args: &[S]) -> Option<String> {
Command::new ("hg")
.env ("HGRCPATH", "")
.env ("LANG", "C")
.args (args)
.output()
.ok()
.and_then (|output|
String::from_utf8 (output.stdout)
.ok())
}
/// Get the version from a mercurial repository.
///
/// Version numbers follow the Python PEP440 conventions. If the
/// current folder corresponds to a version tag, then return that tag.
/// Otherwise, identify the closest tag and return a string of the
/// form _tag_.dev_N_+_hash_. In both cases, if the current folder has
/// been modified, then add the current date as `YYYYMMDD` to the
/// local version label.
fn get_mercurial_version_tag() -> Option<String> {
let output = run_hg (&[ "id", "-i", "-t" ]);
let mut iter = output.iter().flat_map (|s| s.split_whitespace()).fuse();
let hash = match iter.next() {
Some (hash) => hash,
_ => { return None },
};
let clean = !hash.ends_with ("+");
fn mkdate() -> String { time::strftime ("%Y%m%d", &time::now()).unwrap() }
map_for!(
version <- iter.find (|s| s.chars().next()
.map (|c| ('0' <= c) && (c <= '9'))
.unwrap_or (false));
// The current folder corresponds to a version tag (i.e. a
// tag that starts with a digit).
=> (if clean { version.into() }
else { format!("{}+{}", version, mkdate()) }))
.or_else (|| {
// The current folder does not correspond to a version tag.
// Find the closest tag and build the version from that. Note
// that this may return a wrong version number if the closest
// tag is not a version tag.
let version = run_hg (
&[ "parents",
"--template",
"{latesttag}.dev{latesttagdistance}+{node|short}" ]);
if clean { version }
else { version.map (|s| format!("{}.{}", s, mkdate())) }
})
}
/// Get the version from Mercurial archive information.
///
/// The Mercurial `archive` command creates a file named
/// `.hg_archival.txt` that contains information about the archived
/// version. This function tries to use this information to create a
/// version string similar to what `get_mercurial_version_tag` would
/// have created for this version.
fn get_mercurial_archived_version_tag() -> Option<String> {
use map_for::FlatMap;
// Parse the contents of `.hg_archival.txt` into a hash map.
let info = &File::open (".hg_archival.txt")
.iter()
.flat_map (|f| BufReader::new (f).lines())
.filter_map (|l| l.ok())
.map (|l| l.splitn (2, ':')
.map (String::from)
.collect::<Vec<_>>())
.filter_map (
|v| if v.len() == 2
{ Some ((String::from (v[0].trim()),
String::from (v[1].trim()))) }
else { None })
.collect::<HashMap<_,_>>();
// Extract version information from the hash map.
map_for!(
tag <- info.get ("tag");
=> format!("{}+archive.{}", tag, time::strftime ("%Y%m%d", &time::now()).unwrap()))
.or_else (|| map_for!{
tag <- info.get ("latesttag");
distance <- info.get ("latesttagdistance");
node <- info.get ("node");
=> format!("{}.dev{}+archive.{:.12}.{}",
tag, distance, node,
time::strftime ("%Y%m%d", &time::now()).unwrap()) })
.map (String::from)
}
/// Get the version information.
///
/// This function will first try to get the version from a Mercurial
/// repository. If that fails, it will try to get the version from a
/// `.hg_archival.txt` file. If both fail, it will return a version of
/// the form: "unknown-date".
fn get_version() -> String {
get_mercurial_version_tag()
.or_else (get_mercurial_archived_version_tag)
.unwrap_or_else (
|| format!("{}+cargo.{}",
env::var ("CARGO_PKG_VERSION").unwrap(),
time::strftime ("%Y%m%d", &time::now()).unwrap())
.into())
}
fn main()
{
let mut f = File::create ("src/build_info.rs").unwrap();
let version = version_meta().unwrap();
writeln!(f, "pub const RUST_VERSION: &'static str = \"{} {} v{}\";",
env::var ("RUSTC").unwrap_or ("rustc".into()),
match version.channel {
Channel::Dev => "dev",
Channel::Nightly => "nightly",
Channel::Beta => "beta",
Channel::Stable => "stable",
},
version.semver).unwrap();
writeln!(f, "pub const PROFILE: &'static str = \"{}\";",
env::var ("PROFILE").unwrap_or ("unknown".into()))
.unwrap();
writeln!(f, "pub const TARGET: &'static str = \"{}\";",
env::var ("TARGET").unwrap_or ("unknown".into()))
.unwrap();
writeln!(f, "pub const PKG_NAME: &'static str = \"{} {} {}\";",
env::var ("CARGO_PKG_NAME").unwrap(),
get_version(),
env::var ("PROFILE").unwrap_or ("".into()))
.unwrap();
writeln!(f, "pub const PKG_VERSION: &'static str = \"{}\";",
get_version())
.unwrap();
}

How can I override a constant via a compiler option?

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

Resources