Cross-module Variables

I have a Rust project split into 3 files:

  • main.rs
  • config.rs
  • generate.rs

Inside config.rs are variables which are given values by TOML structs. I am required to inject those variables into generate.rs as part of a function within that module. I have imported the config module, yet the compiler throws errors regarding the variables being out-of-scope. The variables are within a function in config.rs as Rust seems to disallow using let in global scope.

How can I allow Rust to inject immutable let variables from another module into a module's function?

Are you looking for pub(crate) const FOO: FooType = /* whatever */?

Would this be used in place of the variables to be exported, or imported?

Could you give an example of how that would look?

There are no global lets, as you noted. Additionally, Rust groups stuff into “modules”, and the things directly inside modules are “items”.

“Items” include function declarations, child modules (mod config; is an item, declaring that a module exists in either config.rs or config/mod.rs), use declarations, struct and enum definitions… and constant items (consts and statics), which is the one relevant here. (let statements are not items, and thus they cannot be contained directly within a module.)

You can see the full list here: Items - The Rust Reference

Here’s some documentation about constant items: constants - Rust By Example

So, these const items would be taking the place of what you tried to use let statements for.

Might also be worth reading about modules and visibility: Control Scope and Privacy with Modules - The Rust Programming Language

Thank you for the explanation. I read the differences between let, const, and static, but my (potentially incorrect) conclusion was that consts cannot have the values injected at runtime (the TOML-based configuration-file in question is where the let values are derived from, on program start), so let seemed to be the only way to do this.

Ah, I had assumed you were injecting the values at compile time. If you’re reading them at runtime, then there’s a lot of options depending on what you’re trying to do.

One option is to just return the config values from the function, and pass them (or a reference to them) to everything that needs to read them. Avoiding runtime global variables is generally preferable.

Yes, I would rather avoid using mutable statics, even if that was an easier way to do this.

For reference, the following is the contents of my config.rs module:

use std::fs;
use serde::Deserialize;


#[derive(Debug, Deserialize)]
struct Config {
        key: Key,
}

#[derive(Debug, Deserialize)]
struct Key {
        ca_priv_key: String,
        user_dir: String,
        cert_serial: String,
}


pub fn config_read() {

        // Get value of "$HOME" environment variable.
        let home_env = std::env::var("HOME").unwrap();
        // Append the configuration directory to the "$HOME" environment-variable value.
        let config_dir = "/.config/signatory";
        // Append the configuration-file filename to the configuration-file directory.
        let config_file = [home_env.clone(), config_dir.to_string(), "/config.toml".to_string()].join("");
        let config_file_parse = fs::read_to_string(&config_file).unwrap();
        let config: Config = toml::from_str(&config_file_parse).unwrap();
        let ca_priv_key = config.key.ca_priv_key;
        let user_dir = config.key.user_dir;
        let cert_serial = config.key.cert_serial;

}

The following is the contents of my generate.rs module:

use std::fs;
use std::fs::OpenOptions;
use std::io;
use std::process::Command;

#[path = "config.rs"]
mod config;


// Function containing the logic for checking whether the certificate-serial-file exists. If it exists, open it. If it
// does not exist, create it, then open it.
fn cert_serial_file_check(path: &str) -> io::Result<std::fs::File> {

	OpenOptions::new()
		.read(true)
		.write(true)
		.create(true)
		.open(path)

}


// Function for reading the contents of the certificate-serial-file. If it contains a string, return that string. If it
// does not contain a value, zero-initialise the string.
fn cert_serial_file_read_init(path: &str) -> String {

	let content = fs::read_to_string(path).unwrap_or_default();
	if content.trim().is_empty() {
		println!("Zero-initialising certificate-serial-file...");
		fs::write(path, "0")
			.unwrap();
		"0".to_string()
	} else {
		content
	}

}


// Function containing the logic for the serial-number incrementation after each OpenSSH-certificate generation.
fn increment(n: &mut u32) {

	*n += 1

}


// Function containing the logic for the OpenSSH-certificate generation.
pub fn generate() {

	let mut serial_read: String;
	let mut serial: u32;

	cert_serial_file_check(cert_serial);

	cert_serial_file_read_init(cert_serial);

	// Convert the string read from the certificate-serial-file to an integer to allow it to be incremented as a number.
	let mut serial: u32 = match serial_read.trim().parse() {
		Ok(num) => num,
		Err(err) => panic!("ERROR: Failed to convert serial-number string to integer!"),
	};

	// Iterate through the defined user OpenSSH public-keys and generate respective OpenSSH certificates for them.
	// The certificate-serial file is read before each certificate generation, in order to use the value as the serial
	// number for the next certificate.
	for user in pub_key.iter() {
		// Begin generation of OpenSSH certificates via looping through defined user OpenSSH public-keys.
		println!("Generating OpenSSH certificate for {}...", user);
		Command::new("ssh-keygen")
			.args([
				// OpenSSH certificate-authority private-key to use for signing the user's OpenSSH public-key.
				"s", ca_priv_key,
				// User's OpenSSH public-key to signed.
				"i", user,
				// OpenSSH principles to be granted access to on remote systems.
				"n", principle,
				// OpenSSH-certificate validity dates.
				"v", validity,
				// Clear all default OpenSSH permissions for the OpenSSH certificate.
				"O", "clear",
				// Permit pty access via the OpenSSH certificate.
				"O", "permit-pty",
				// Set the OpenSSH-certificate serial number to the number contained in the certificate-serial file.
				"z", serial,
				// Path to the user's OpenSSH public-key.
				pub_key,
			])
			.output()
			.expect("Failed to generate OpenSSH certificate for user!");
		increment(&mut serial);

	}

}

Both modules are incomplete, with other issues, but I am currently focussing on getting the config.rs variables to be read inside generate.rs.

So the idea is that this is called once, and afterwards you just use the configured values?

I think your main options are creating an Arc<Config> you can cheaply clone and pass around and store in structs, or perhaps use a static LazyLock if you only need one configuration and you need it ~everywhere.

Something like...

pub fn config_read() -> Result<Config, ..> { ... }

// Need multiple `Config` or want better encapsulation
pub fn main_or_early_on_elswehere() -> Result<..> {
    let config = Arc::new(config_read()?);
    // ...
}

// Need just one and don't mind globals
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| config_read().unwrap());

So the idea is that this is called once, and afterwards you just use the configured values?

Yes. This is my first major project written in Rust, after some time developing small script-like programs. The intention is to use a TOML-file to inject values into the variables, then have the variables be used within the generation code. That way, paths and other configuration options can be declared by each user, rather than hard-coded.

In case it's not clear yet, there is no way to inject values into some other function. You'll need something nameable in the generate function one way or another.

  • Some function you call to get the data (each time)
  • Some global you access to get the data
  • An argument to the function you access
  • Make generate a method on some struct that stores required data and access your fields
  • Etc

My advice for any of these is to make a pub struct that has a field for each variable you wish you could inject into functions like generate (maybe that's your current Config, maybe it's something else, hard to say). Reading the config will create the struct...

    // Honestly this should be a `Result<Config, ..>` for the inevitable
    // invalid config file or whatever, but that's another exercise.
    pub fn read_config() -> Config { ... }

...and then the various approach look like

  • Some function you call to get the data (each time)
    pub fn generate() {
        let config = crate::config::read_config();
        // ...
    }
    
  • Some global you access to get the data
    // config.rs
    pub static CONFIG: LazyLock<Config> = LazyLock::new(|| {
        read_config()
    });
    // (Also make `fn read_config` private if you wish)
    
    // generate.rs
    pub fn generate() {
        // Or just import and use `CONFIG` directly
        let config = &*crate::config::CONFIG;
        // ...
    }
    
  • An argument to the function you access
    use crate::config::Config;
    pub fn generate(config: &Config) {
        // ...
    }
    
  • Make generate a method on some struct that stores required data and access your fields
    pub struct Generator {
        // Or `Rc<Config>` / `Arc<Config>` if you need this in many types
        // and don't want to pay the cloning cost
        config: Config,
    }
    
    impl Generator {
        fn new(config: Config) -> Self { ... }
        fn generate(&self) {
            // (Just access `self.config` as needed)
            // ...
        }
    }
    

Yes, that is what my current Config struct is for. I tested it before splitting the large main.rs module into 3 separate modules for modularity, and debug-text via println! showed that the configuration-file was indeed populating the structs.

I believe the "Some global you access to get the data" approach would work best, as it is an immutable variable, so should avoid the issues of a mutable one.

The rest of the code can remain intact with that approach, yes? Assuming I place

let config = &*crate::config::CONFIG;

at the top of the public generate() function?

Correct, you won't have to add signature or so on if you use a global.

I have implemented the global-static approach. At this point, the compiler throws an error regarding cert_serial "not found in this scope", which I believe means it's not able to find the variable?

We’ll need to see the code to understand what the problem is and what advice you need — at least

  • the function containing the error
  • the definition of struct Config
  • the definition of the static

Compiler errors:

    Checking signatory v0.1.0 (/mnt/a-00-net/git/inferencium/signatory)
error[E0425]: cannot find value `cert_serial` in this scope
  --> src/generate.rs:56:25
   |
56 |     cert_serial_file_check(cert_serial);
   |                            ^^^^^^^^^^^ not found in this scope

error[E0425]: cannot find value `cert_serial` in this scope
  --> src/generate.rs:58:29
   |
58 |     cert_serial_file_read_init(cert_serial);
   |                                ^^^^^^^^^^^ not found in this scope

error[E0425]: cannot find value `pub_key` in this scope
  --> src/generate.rs:69:14
   |
69 |     for user in pub_key.iter() {
   |                 ^^^^^^^ not found in this scope

error[E0425]: cannot find value `ca_priv_key` in this scope
  --> src/generate.rs:75:10
   |
75 |                 "s", ca_priv_key,
   |                      ^^^^^^^^^^^ not found in this scope

error[E0425]: cannot find value `principle` in this scope
  --> src/generate.rs:79:10
   |
79 |                 "n", principle,
   |                      ^^^^^^^^^ not found in this scope

error[E0425]: cannot find value `validity` in this scope
  --> src/generate.rs:84:10
   |
84 |                 "v", validity,
   |                      ^^^^^^^^ not found in this scope

error[E0425]: cannot find value `pub_key` in this scope
  --> src/generate.rs:92:5
   |
92 |                 pub_key,
   |                 ^^^^^^^ not found in this scope

error[E0603]: function `config_read` is private
  --> src/main.rs:14:10
   |
14 |     config::config_read();
   |             ^^^^^^^^^^^ private function
   |
note: the function `config_read` is defined here
  --> src/config.rs:19:1
   |
19 | fn config_read() {
   | ^^^^^^^^^^^^^^^^

error: missing type for `static mut` item
  --> src/config.rs:24:23
   |
24 |     static mut config_dir = "/.config/signatory";
   |                          ^ help: provide a type for the static variable: `: &str`

error[E0121]: the placeholder `_` is not allowed within types on item signatures for static variables
  --> src/config.rs:24:23
   |
24 |     static mut config_dir = "/.config/signatory";
   |                          ^ not allowed in type signatures
   |
help: replace this with a fully-specified type
   |
24 |     static mut config_dir&str = "/.config/signatory";
   |                          ++++

error[E0308]: mismatched types
  --> src/config.rs:37:2
   |
37 |     config_read()
   |     ^^^^^^^^^^^^^ expected `Config`, found `()`

Some errors have detailed explanations: E0121, E0308, E0425, E0603.
For more information about an error, try `rustc --explain E0121`.
error: could not compile `signatory` (bin "signatory") due to 12 previous errors

I am aware that some of the arguments have not yet been implemented in the TOML configuration-file, thus not being in the structs. The for-loop has also not been fully implemented, yet. The other errors, regarding existing variables within config.rs being accessed by generate.rs, are what are new to me.

main.rs:

mod config;
mod generate;


fn main() {

	config::config_read();

	generate::generate();

}

config.rs:

use std::fs;
use std::sync::LazyLock;
use serde::Deserialize;


#[derive(Debug, Deserialize)]
struct Config {
	key: Key,
}

#[derive(Debug, Deserialize)]
struct Key {
	ca_priv_key: String,
	user_dir: String,
	cert_serial: String,
}


fn config_read() {

	// Get value of "$HOME" environment variable.
	let home_env = std::env::var("HOME").unwrap();
	// Append the configuration directory to the "$HOME" environment-variable value.
	static mut config_dir = "/.config/signatory";
	// Append the configuration-file filename to the configuration-file directory.
	let config_file = [home_env.clone(), config_dir.to_string(), "/config.toml".to_string()].join("");
	let config_file_parse = fs::read_to_string(&config_file).unwrap();
	let config: Config = toml::from_str(&config_file_parse).unwrap();
	let ca_priv_key = config.key.ca_priv_key;
	let user_dir = config.key.user_dir;
	let cert_serial = config.key.cert_serial;

}

pub static CONFIG: LazyLock<Config> = LazyLock::new (|| {

	config_read()

});

generate.rs:

use std::fs;
use std::fs::OpenOptions;
use std::io;
use std::process::Command;

#[path = "config.rs"]
mod config;


// Function containing the logic for checking whether the certificate-serial-file exists. If it exists, open it. If it
// does not exist, create it, then open it.
fn cert_serial_file_check(path: &str) -> io::Result<std::fs::File> {

	OpenOptions::new()
		.read(true)
		.write(true)
		.create(true)
		.open(path)

}


// Function for reading the contents of the certificate-serial-file. If it contains a string, return that string. If it
// does not contain a value, zero-initialise the string.
fn cert_serial_file_read_init(path: &str) -> String {

	let content = fs::read_to_string(path).unwrap_or_default();
	if content.trim().is_empty() {
		println!("Zero-initialising certificate-serial-file...");
		fs::write(path, "0")
			.unwrap();
		"0".to_string()
	} else {
		content
	}

}


// Function containing the logic for the serial-number incrementation after each OpenSSH-certificate generation.
fn increment(n: &mut u32) {

	*n += 1

}


// Function containing the logic for the OpenSSH-certificate generation.
pub fn generate() {

	let config = &*crate::config::CONFIG;

	let mut serial_read: String;
	let mut serial: u32;

	cert_serial_file_check(cert_serial);

	cert_serial_file_read_init(cert_serial);

	// Convert the string read from the certificate-serial-file to an integer to allow it to be incremented as a number.
	let mut serial: u32 = match serial_read.trim().parse() {
		Ok(num) => num,
		Err(err) => panic!("ERROR: Failed to convert serial-number string to integer!"),
	};

	// Iterate through the defined user OpenSSH public-keys and generate respective OpenSSH certificates for them.
	// The certificate-serial file is read before each certificate generation, in order to use the value as the serial
	// number for the next certificate.
	for user in pub_key.iter() {
		// Begin generation of OpenSSH certificates via looping through defined user OpenSSH public-keys.
		println!("Generating OpenSSH certificate for {}...", user);
		Command::new("ssh-keygen")
			.args([
				// OpenSSH certificate-authority private-key to use for signing the user's OpenSSH public-key.
				"s", ca_priv_key,
				// User's OpenSSH public-key to signed.
				"i", user,
				// OpenSSH principles to be granted access to on remote systems.
				"n", principle,
				// OpenSSH-certificate validity dates.
				"v", validity,
				// Clear all default OpenSSH permissions for the OpenSSH certificate.
				"O", "clear",
				// Permit pty access via the OpenSSH certificate.
				"O", "permit-pty",
				// Set the OpenSSH-certificate serial number to the number contained in the certificate-serial file.
				"z", serial,
				// Path to the user's OpenSSH public-key.
				pub_key,
			])
			.output()
			.expect("Failed to generate OpenSSH certificate for user!");
		increment(&mut serial);

	}

}

You defined the static, and mentioned the static, but you aren't using the Config stored in the static.

cert_serial_file_check(cert_serial);

needs to be instead

cert_serial_file_check(&config.key.cert_serial);

Or if you particularly want to split out the entire Config to local variables, you can do that:

let Config {
    key: Key {
        ca_priv_key,
        user_dir,
        cert_serial,
    }
} = &*crate::config::CONFIG;

cert_serial_file_check(cert_serial);

I still don't fully understand the & character versus not using it. I guess more coding in Rust will let me get a better understanding of it. I am mostly relying on compiler output for that character.

I did add the fixes you mentioned, but it throws an error about them being private fields. The structs were changed the pub struct within config.rs as an attempted fix, but the error remains.

You need to make the individual fields public.

#[derive(Debug, Deserialize)]
pub struct Config {
    pub key: Key,
}

#[derive(Debug, Deserialize)]
pub struct Key {
    pub ca_priv_key: String,
    pub user_dir: String,
    pub cert_serial: String,
}

The full error (from cargo build in a terminal) probably suggests a fix. But if it doesn't, or the fix doesn't work, include the full error in a comment and someone can probably help.

The solution from @kpreid resolved the issue. It seems that I now need to only complete the structs for the configuration-file, and the for-loop. I am down to 7 errors from 12 errors, with the remaining ones seemingly being for what is not yet implemented.