Parameter Code Generation

Generate consistent parameter indices for Rust and C++ from a single source.

Build Script Integration

The recommended approach is to generate code at build time using a build.rs script.

Using parameters.json

// build.rs
use std::env;
use std::fs;
use std::path::Path;

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();

    // Read parameters.json
    let json = fs::read_to_string("parameters.json")
        .expect("Failed to read parameters.json");

    let params: serde_json::Value = serde_json::from_str(&json)
        .expect("Failed to parse parameters.json");

    // Generate Rust constants
    let mut rust_code = String::from("// Auto-generated - DO NOT EDIT\n\n");
    if let Some(parameters) = params["parameters"].as_array() {
        for (index, param) in parameters.iter().enumerate() {
            let id = param["id"].as_str().unwrap();
            rust_code.push_str(&format!("pub const PARAM_{}: usize = {};\n", id, index));
        }
        rust_code.push_str(&format!("\npub const PARAM_COUNT: usize = {};\n", parameters.len()));
    }

    // Write to OUT_DIR
    let dest_path = Path::new(&out_dir).join("params.rs");
    fs::write(&dest_path, rust_code).unwrap();

    // Generate C header (prefer using ParamsFile API instead - see below)
    let mut c_header = String::from("/* Auto-generated - DO NOT EDIT */\n\n");
    c_header.push_str("#ifndef BBX_PARAMS_H\n#define BBX_PARAMS_H\n\n");
    if let Some(parameters) = params["parameters"].as_array() {
        for (index, param) in parameters.iter().enumerate() {
            let id = param["id"].as_str().unwrap();
            c_header.push_str(&format!("#define PARAM_{} {}\n", id, index));
        }
        c_header.push_str(&format!("\n#define PARAM_COUNT {}\n\n", parameters.len()));

        // Generate PARAM_IDS array for dynamic iteration
        c_header.push_str("static const char* PARAM_IDS[PARAM_COUNT] = {\n");
        for (i, param) in parameters.iter().enumerate() {
            let id = param["id"].as_str().unwrap();
            let comma = if i < parameters.len() - 1 { "," } else { "" };
            c_header.push_str(&format!("    \"{}\"{}\n", id, comma));
        }
        c_header.push_str("};\n");
    }
    c_header.push_str("\n#endif /* BBX_PARAMS_H */\n");

    // Write to include directory
    fs::write("include/bbx_params.h", c_header).unwrap();

    println!("cargo:rerun-if-changed=parameters.json");
}

Including Generated Code

In your lib.rs:

#![allow(unused)]
fn main() {
// Include the generated parameter constants
include!(concat!(env!("OUT_DIR"), "/params.rs"));
}

Using ParamsFile API

Alternatively, use the built-in API:

// build.rs
use bbx_plugin::ParamsFile;
use std::fs;

fn main() {
    let json = fs::read_to_string("parameters.json").unwrap();
    let params = ParamsFile::from_json(&json).unwrap();

    // Generate Rust code
    let rust_code = params.generate_rust_indices();
    fs::write(format!("{}/params.rs", std::env::var("OUT_DIR").unwrap()), rust_code).unwrap();

    // Generate C header
    let c_header = params.generate_c_header();
    fs::write("include/bbx_params.h", c_header).unwrap();

    println!("cargo:rerun-if-changed=parameters.json");
}

Using Programmatic Definitions

For compile-time definitions:

// build.rs
use bbx_plugin::{ParamDef, generate_rust_indices_from_defs, generate_c_header_from_defs};
use std::fs;

const PARAMETERS: &[ParamDef] = &[
    ParamDef::float("GAIN", "Gain", -60.0, 30.0, 0.0),
    ParamDef::bool("MONO", "Mono", false),
];

fn main() {
    let rust_code = generate_rust_indices_from_defs(PARAMETERS);
    fs::write(format!("{}/params.rs", std::env::var("OUT_DIR").unwrap()), rust_code).unwrap();

    let c_header = generate_c_header_from_defs(PARAMETERS);
    fs::write("include/bbx_params.h", c_header).unwrap();
}

CMake Integration

Include the generated header in CMake:

# Ensure Rust build runs first (Corrosion handles this)
corrosion_import_crate(MANIFEST_PATH dsp/Cargo.toml)

# Include the generated header directory
target_include_directories(${PLUGIN_TARGET} PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/dsp/include)

Using PARAM_IDS in C++

The generated header includes a PARAM_IDS array for dynamic parameter iteration:

// Generated header contains:
// static const char* PARAM_IDS[PARAM_COUNT] = { "GAIN", "MONO", ... };

Caching Parameter Pointers

Cache atomic pointers once in the constructor:

// In processor.h
std::array<std::atomic<float>*, PARAM_COUNT> m_paramPointers {};

// In processor.cpp constructor
for (size_t i = 0; i < PARAM_COUNT; ++i) {
    m_paramPointers[i] = m_parameters.getRawParameterValue(juce::String(PARAM_IDS[i]));
}

Loading Parameters in processBlock

Replace manual per-parameter loading with a loop:

// Instead of:
// auto* gain = m_parameters.getRawParameterValue("GAIN");
// m_paramBuffer[PARAM_GAIN] = gain ? gain->load() : 0.0f;
// ... repeated for each parameter

// Use:
for (size_t i = 0; i < PARAM_COUNT; ++i) {
    m_paramBuffer[i] = m_paramPointers[i] ? m_paramPointers[i]->load() : 0.0f;
}

This eliminates manual C++ updates when adding parameters.

Verification

Add a test to verify Rust and C++ constants match:

#![allow(unused)]
fn main() {
#[test]
fn test_param_indices_match() {
    // If this compiles, indices are in sync
    assert_eq!(PARAM_COUNT, 7);
    assert!(PARAM_GAIN < PARAM_COUNT);
    assert!(PARAM_PAN < PARAM_COUNT);
}
}