cplex-sys 0.9.1

Low level bindings to the Cplex C-API
/*
 * MIT License
 *
 * Copyright (c) 2017-2022 Frank Fischer
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::{Read, Result, Write};
use std::iter::FromIterator;
use std::path::Path;

extern crate regex;
use regex::Regex;

const DEPRECATED_PARAMS: [(&str, &str); 2] = [
    ("Benders_Tolerances_feasibilitycut", "Benders_Tolerances_FeasibilityCut"),
    ("Benders_Tolerances_optimalitycut", "Benders_Tolerances_OptimalityCut"),
];

struct Constants<'a> {
    consts: HashMap<&'a str, &'a str>,
    args: HashMap<&'a str, &'a str>,
    enums: HashMap<&'a str, Vec<(&'a str, Option<&'a str>)>>,
}

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let out_dir = env::var("OUT_DIR")?;
    let out_dir = Path::new(&out_dir);
    let cplex_dir = match env::var("CPLEX_HOME") {
        Ok(cpx) => {
            println!("cargo:rustc-link-search={}/lib/x86-64_sles10_4.1/static_pic", cpx);
            println!("cargo:rustc-link-search={}/lib/x86-64_linux/static_pic", cpx);
            println!("cargo:rustc-link-lib=cplex");
            Path::new(&cpx).join("include").join("ilcplex")
        }
        Err(_) => {
            println!("cargo:rustc-link-lib=cplex");
            if let Some(dir) = ["/usr/include", "/usr/local/include"].iter().find(|dir| {
                let p = Path::new(dir).join("ilcplex");
                let f = p.join("cplex.h");
                f.exists()
            }) {
                Path::new(dir).join("ilcplex")
            } else {
                panic!("Can't find include file `ilcplex/cplex.h` (maybe set CPLEX_HOME environment variable)");
            }
        }
    };

    let mut text = String::new();
    File::open(cplex_dir.join("cpxconst.h"))?.read_to_string(&mut text)?;

    let constants = {
        Constants {
            args: parse_cpxconst_args(&text),
            consts: parse_cpxconst_enum(&text),
            enums: parse_cpxconst_enums(&text),
        }
    };

    {
        let mut f = File::create(out_dir.join("cplex-extern.rs"))?;
        parse_cplex(cplex_dir.join("cplex.h").to_str().unwrap(), &constants, &mut f)?;
    }
    Ok(())
}

fn parse_cpxconst_args(text: &str) -> HashMap<&str, &str> {
    let re_args = Regex::new(r"#define\s+(CALLBACK_[A-Z]+_ARGS)\s+((?:[^\\\n]|\\\n)*)").unwrap();
    HashMap::from_iter(
        re_args
            .captures_iter(text)
            .map(|caps| (caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())),
    )
}

fn parse_cpxconst_enum(text: &str) -> HashMap<&str, &str> {
    // We ignore CPX_PARAM_* and use the modern form CPXPARAM_* instead.
    // Furthermore we also parse CPXMIP_* constants as extension to CPX_STAT_*.
    let re_args = Regex::new(
        r#"#define\s+CPX(_[A-Za-z_]+|(?:MIP|PARAM)_[A-Za-z_]+)\s*([-+eE0-9.]+[lL]*|NAN|'[^']'|"[^\n"]*")\n"#,
    )
    .unwrap();
    HashMap::from_iter(re_args.captures_iter(text).filter_map(|caps| {
        let name = caps.get(1).unwrap().as_str();
        let value = caps.get(2).unwrap().as_str();
        if name.starts_with("_PARAM") || name == "PARAM_H" {
            None
        } else if name.starts_with("PARAM") || name.starts_with("MIP") {
            Some((name, value))
        } else {
            Some((&name[1..], value))
        }
    }))
}

fn parse_cpxconst_enums(text: &str) -> HashMap<&str, Vec<(&str, Option<&str>)>> {
    let re_enums = Regex::new(r"typedef\s+enum\s*\{((?:\s+CPX[- \tA-Z_=0-9]+,?)+\s*)}\s*(CPX[A-Z_]+)\s*;").unwrap();
    let re_fields = Regex::new(r"CPX[A-Z]+_([A-Z_]+)(?:\s+=\s+([-0-9]+))?").unwrap();

    HashMap::from_iter(re_enums.captures_iter(text).map(|caps| {
        (
            caps.get(2).unwrap().as_str(),
            re_fields
                .captures_iter(caps.get(1).unwrap().as_str())
                .map(|field| (field.get(1).unwrap().as_str(), field.get(2).map(|s| s.as_str())))
                .collect(),
        )
    }))
}

fn parse_cplex<W: Write>(filename: &str, constants: &Constants, f: &mut W) -> Result<()> {
    let mut text = String::new();
    File::open(filename)?.read_to_string(&mut text)?;

    let re =
        Regex::new(r"CPXLIBAPI\s+(?P<return>\w+)\s+CPXPUBLIC\s+(?P<name>CPX[[:alpha:]]+)\s*\((?P<params>[^;]+)\)\s*;")
            .unwrap();
    let re_params = Regex::new(r"(?P<type>\w[^,()]*)(?P<name>\b[0-9a-zA-Z_]+)\s*(?:,|$)|(?P<freturn>\w[^()]*)\(CPXPUBLIC\s*(?P<ftype>\*+)\s*(?P<fname>\w+)\s*\)\s*\((?P<fparams>[^)]*)\)\s*(?:,|$)").unwrap();

    writeln!(f, "use std::f64::NAN;")?;
    writeln!(f, "pub enum Env {{}}")?;
    writeln!(f, "pub enum Lp {{}}")?;
    writeln!(f, "pub enum Net {{}}")?;
    writeln!(f, "pub enum Channel {{}}")?;
    writeln!(f, "pub enum Serializer {{}}")?;
    writeln!(f, "pub enum Deserializer {{}}")?;
    writeln!(f, "pub enum CallbackContext {{}}")?;
    writeln!(f, "pub enum ParamSet {{}}")?;

    let mut stats = HashMap::new();
    let mut mip = HashMap::new();
    let mut params = HashMap::new();
    let mut algs = HashMap::new();
    let mut defines = HashMap::new();

    for (var, val) in constants.consts.iter() {
        if let Some(var) = var.strip_prefix("STAT_") {
            stats.insert(var, val.parse::<i64>().unwrap());
        } else if let Some(var) = var.strip_prefix("MIP_") {
            mip.insert(var, val.parse::<i64>().unwrap());
        } else if let Some(var) = var.strip_prefix("PARAM_") {
            params.insert(var, val.parse::<i64>().unwrap());
        } else if let Some(var) = var.strip_prefix("ALG_") {
            algs.insert(var, val.parse::<i64>().unwrap());
        } else {
            defines.insert(var, val);
        }
    }

    fix_params(&mut params);

    write_enum(f, "Stat", &stats)?;
    write_enum(f, "Mip", &mip)?;
    write_enum(f, "Param", &params)?;
    write_enum(f, "Alg", &algs)?;

    for (name, fields) in &constants.enums {
        write_c_enum(f, name, fields)?;
    }

    for (var, &val) in defines.iter() {
        let (typ, val, postfix) = if val.starts_with('\'') && val.ends_with('\'') {
            ("c_char", &val[..], " as c_char")
        } else if val.starts_with('\"') && val.ends_with('\"') {
            ("&str", &val[..], "")
        } else if let Some(val) = val.strip_suffix("LL") {
            ("c_longlong", val, "")
        } else if val.contains('e') || val.contains('E') || val == &"NAN" {
            ("c_double", &val[..], "")
        } else {
            ("c_int", &val[..], "")
        };
        writeln!(f, "pub const {} : {} = {}{};", var, typ, val, postfix)?;
    }

    writeln!(f, "extern \"C\" {{")?;

    for caps in re.captures_iter(&text) {
        writeln!(f, "    #[link_name = \"{}\"]", &caps["name"])?;
        write!(f, "    pub fn {}(", &caps["name"][3..])?;

        for param in re_params.captures_iter(&caps["params"]) {
            if let Some(name) = param.name("name").map(|m| m.as_str()) {
                write!(f, "{}: {}, ", c_to_rust_name(name), c_to_rust_type(&param["type"]))?;
            } else {
                match &param["ftype"] {
                    "*" => write!(f, "{}: Option<extern fn(", &param["fname"])?,
                    "**" => write!(f, "{}: *mut Option<extern fn(", &param["fname"])?,
                    _ => panic!("Unsupported function pointer type for {}", &caps["name"]),
                }

                if let Some(fparams) = constants.args.get(&param["fparams"]) {
                    for fparam in re_params.captures_iter(fparams) {
                        write!(f, "{}, ", c_to_rust_type(&fparam["type"]))?;
                    }
                } else {
                    for fparam in param["fparams"].split(',').map(|fp| fp.trim()) {
                        write!(f, "{}, ", c_to_rust_type(fparam))?;
                    }
                }
                if param["freturn"].trim() == "void" {
                    write!(f, ")>, ")?;
                } else {
                    write!(f, ") -> {}>, ", c_to_rust_type(&param["freturn"]))?;
                }
            }
        }

        write!(f, ")")?;

        if caps["return"].trim() != "void" {
            write!(f, " -> {}", c_to_rust_type(&caps["return"]))?;
        }

        writeln!(f, ";")?;
    }
    writeln!(f, "}}")?;

    Ok(())
}

fn c_to_rust_type(ctype: &str) -> &str {
    let ctype = ctype.trim();
    match ctype {
        "int" | "CPXINT" => "c_int",
        "int *" | "CPXINT *" | "volatile int *" => "*mut c_int",
        "const int *" | "int const *" => "*const c_int",
        "CPXLONG" => "c_longlong",
        "CPXLONG *" => "*mut c_longlong",
        "CPXLONG const *" => "*const c_longlong",
        "double" => "c_double",
        "double *" => "*mut c_double",
        "const double *" | "double const *" => "*const c_double",
        "char" => "c_char",
        "const char *" | "char const *" | "CPXCCHARptr" => "*const c_char",
        "char *" | "CPXCHARptr" => "*mut c_char",
        "char **" | "char  **" => "*const *const c_char",
        "void *" => "*mut c_void",
        "void const *" => "*const c_void",
        "void **" | "void  **" => "*mut *mut c_void",
        "CPXENVptr" => "*mut Env",
        "CPXENVptr *" => "*mut *mut Env",
        "CPXCENVptr" => "*const Env",
        "CPXLPptr" => "*mut Lp",
        "CPXLPptr *" => "*mut *mut Lp",
        "CPXCLPptr" => "*const Lp",
        "CPXCLPptr *" => "*mut *const Lp",
        "CPXNETptr" => "*mut Net",
        "CPXNETptr *" => "*mut *mut Net",
        "CPXINFOTYPE" => "InfoType",
        "CPXCALLBACKCONTEXTptr" => "*mut CallbackContext",
        "CPXCALLBACKFUNC" => "extern fn(*mut CallbackContext, c_longlong, *mut c_void) -> c_int",
        "CPXCALLBACKFUNC **" => "*mut extern fn(*mut CallbackContext, c_longlong, *mut c_void) -> c_int",
        "CPXCALLBACKINFO" => "CallbackInfo",
        "CPXCALLBACKSOLUTIONSTRATEGY" => "CallbackSolutionStrategy",
        "CPXMODELASSTCALLBACKFUNC" => "extern fn(c_int, *const c_char, *mut c_void) -> c_int",
        "CPXMODELASSTCALLBACKFUNC **" => "*mut extern fn(c_int, *const c_char, *mut c_void) -> c_int",
        "CPXCNETptr" => "*const Net",
        "CPXCHANNELptr" => "*mut Channel",
        "CPXCHANNELptr *" => "*mut *mut Channel",
        "CPXCPARAMSETptr" => "*const ParamSet",
        "CPXPARAMSETptr" => "*mut ParamSet",
        "CPXPARAMSETptr *" => "*mut *mut ParamSet",
        "CPXCPARAMSETptr const *" => "*const *const ParamSet",
        "CPXFILEptr" => "*mut File",
        "CPXFILEptr *" => "*mut *mut File",
        "CPXSERIALIZERptr" => "*mut Serializer",
        "CPXSERIALIZERptr *" => "*mut *mut Serializer",
        "CPXCSERIALIZERptr" => "*const Serializer",
        "CPXDESERIALIZERptr" => "*mut Deserializer",
        "CPXDESERIALIZERptr *" => "*mut *mut Deserializer",
        "CPXCDESERIALIZERptr" => "*const Deserializer",
        _ => panic!("Unknown C type: {}", ctype),
    }
}

fn c_to_rust_name(name: &str) -> &str {
    match name {
        "type" => "typ",
        _ => name,
    }
}

fn to_camel_case(s: &str) -> String {
    // only camelize if the name does not contain a small letter

    // This is a very special case, the only constant (so far)
    // that mixes camelcase and snakecase for some stupid reason.
    if let Some(name) = s.strip_suffix("INForUNBD") {
        return to_camel_case(&format!("{}_INF_OR_UNBD", name));
    }

    if s.chars().any(|c| c.is_lowercase()) {
        s.to_string()
    } else {
        let mut result = String::new();
        let mut upcase = true;
        for c in s.chars() {
            if c == '_' {
                upcase = true;
            } else if upcase {
                result.extend(c.to_uppercase());
                upcase = false;
            } else {
                result.extend(c.to_lowercase());
                upcase = false;
            }
        }
        result
    }
}

fn write_enum<W: Write>(f: &mut W, name: &str, values: &HashMap<&str, i64>) -> Result<()> {
    writeln!(f, "#[allow(non_camel_case_types)]")?;
    writeln!(f, "#[derive(Clone, Copy, PartialEq, Eq, Debug)]\npub enum {} {{", name)?;
    let mut values = values.iter().collect::<Vec<_>>();
    values.sort_by_key(|x| x.1);
    for &(name, num) in &values {
        writeln!(f, "    {} = {},", to_camel_case(name), num)?;
    }
    writeln!(f, "}}")?;

    writeln!(f, "impl {} {{ pub fn to_c(self) -> c_int {{ self as c_int }} }}", name)?;

    Ok(())
}

fn write_c_enum<W: Write>(f: &mut W, name: &str, fields: &[(&str, Option<&str>)]) -> Result<()> {
    writeln!(
        f,
        "#[derive(Clone, Copy, PartialEq, Eq, Debug)]\n#[repr(C)]\npub enum {} {{",
        c_to_rust_type(name),
    )?;

    for field in fields {
        write!(f, "    {}", to_camel_case(field.0))?;
        if let Some(default) = &field.1 {
            write!(f, " = {}", default)?;
        }
        writeln!(f, ",")?;
    }
    writeln!(f, "}}")?;

    Ok(())
}

fn fix_params(params: &mut HashMap<&str, i64>) {
    for &(old, new) in &DEPRECATED_PARAMS[..] {
        if let Some(old_val) = params.remove(old) {
            if let Some(new_val) = params.insert(new, old_val) {
                if new_val != old_val {
                    panic!("Different values for {} and {}", old, new);
                }
            }
        }
    }
}