Skip to main content

choco_solver/
lib.rs

1//! Safe Rust wrapper around the [Choco Solver](https://choco-solver.org/) API.
2//!
3//! This crate provides an idiomatic Rust interface to model and solve constraint
4//! satisfaction/optimization problems while delegating solving to Choco through
5//! the native `libchoco_capi` library.
6//!
7//! For complete solver semantics and advanced API details, refer to the official
8//! Choco documentation: <https://choco-solver.org/>.
9//!
10//! # Getting `libchoco_capi.dll`
11//!
12//! You can obtain the native DLL in two main ways:
13//! - Build/fetch it from the upstream project:
14//!   [choco-solver-capi](https://github.com/chocoteam/choco-solver-capi)
15//! - Clone this repository and follow [BUILDING](./BUILDING.md)
16//!   (for example, `cargo xtask build-dll`)
17//!
18//! # Where to put the DLL
19//!
20//! `libchoco_capi.dll` must be discoverable by the dynamic loader. Typical options:
21//! - Place it in a folder already present in your system DLL search path.
22//! - Set `CHOCO_SOLVER_DLL_FOLDER` to the DLL directory.
23//! - Call [`ChocoBackend::set_dll_folder`] before creating/using models.
24//!
25//! # Scope of this crate
26//!
27//! This crate is a safe wrapper on top of the Choco solver C API (`choco-solver-sys`
28//! is the unsafe FFI boundary). The high-level API is Rust-oriented, but solver
29//! behavior is defined by Choco itself.
30//!
31//! # Warning: Avoid mixing variables from different models
32//! <div class="warning">
33//! Do not mix variables from different models.
34//! The wrapper currently does not prevent cross-model variable mixing in constraints
35//! and relies on backend behavior.
36//! </div>
37//!
38//! # Thread safety
39//!
40//! This library creates one separate GraalVM isolate (independent execution
41//! environment) per process.
42//! Currently the all types are not Send and Sync until it is clarified the thread safety of Choco Solver API and GraalVM native C API.
43//! However, it is possible to create and use models from multiple threads as long as they are not shared across threads (i.e., each thread creates and uses its own models and variables).
44//! The library ensures that the GraalVM isolate is initialized and attached to each thread that interacts with the Choco backend, allowing for concurrent usage from multiple threads without sharing state between them.
45//!
46//!
47//! # Example
48//!
49//! ```no_run
50//! use choco_solver::*;
51//!
52//! fn main() {
53//!     // Optional: point to directory containing `libchoco_capi.dll`.
54//!     // ChocoBackend::set_dll_folder("C:/path/to/dll/folder".to_string());
55//!
56//!     let model = Model::new(Some("DemoModel"));
57//!     let x = model.int_var_bounded(0, 200, Some("x"), None);
58//!     let y = model.int_var_bounded(0, 200, Some("y"), None);
59//!     let sum_is_156 = (&x + &y).eq(156).reify();
60//!
61//!     true.eq(&sum_is_156).post().expect("failed to post constraint");
62//!
63//!     let solver = model.solver();
64//!     let solution = solver
65//!         .find_solution(&Criterion::default())
66//!         .expect("failed to find solution");
67//!
68//!     let bx = solution.get_int_var(&x).expect("x not available");
69//!     let by = solution.get_int_var(&y).expect("y not available");
70//!     let bsum = solution
71//!         .get_bool_var(&sum_is_156)
72//!         .expect("reified bool not available");
73//!
74//!     println!("solution: x = {bx}, y = {by}, x + y = {}, reified = {bsum}", bx + by);
75//! }
76//! ```
77
78pub(crate) mod constraint;
79pub(crate) mod model;
80pub(crate) mod solution;
81pub(crate) mod solver;
82pub(crate) mod utils;
83pub(crate) mod variables;
84
85// Re-export main modules
86pub use constraint::*;
87pub use model::*;
88pub use solution::*;
89pub use solver::*;
90pub use variables::*;
91
92use choco_solver_sys::{graal_isolate_t, graal_isolatethread_t, libchoco_capi, library_filename};
93use std::{
94    path::Path,
95    ptr,
96    sync::{LazyLock, Mutex, OnceLock},
97};
98use thiserror::Error;
99#[cfg(target_os = "windows")]
100const CHOCO_SOLVER_LIB_NAME: &str = "libchoco_capi";
101#[cfg(not(target_os = "windows"))]
102const CHOCO_SOLVER_LIB_NAME: &str = "choco_capi";
103
104/// Global variable to store the DLL folder path, if set. This is used to configure the directory from which the `libchoco_capi` library will be loaded.
105/// It is initialized lazily when the library is first accessed, and can be set using `ChocoBackend::set_dll_folder`.
106static DLL_FOLDER: OnceLock<Option<String>> = OnceLock::new();
107
108static CHOCO_LIB: LazyLock<libchoco_capi> = LazyLock::new(|| {
109    let dll_folder = DLL_FOLDER.get_or_init(|| std::env::var("CHOCO_SOLVER_DLL_FOLDER").ok());
110    unsafe {
111        match dll_folder {
112            Some(folder) => {
113                libchoco_capi::new(Path::new(&folder).join(library_filename(CHOCO_SOLVER_LIB_NAME)))
114                    .expect("Failed to load Choco C API library")
115            }
116            None => libchoco_capi::new(library_filename(CHOCO_SOLVER_LIB_NAME))
117                .expect("Failed to load Choco C API library"),
118        }
119    }
120});
121
122#[repr(transparent)]
123struct IsolatePtr(Option<*mut graal_isolate_t>);
124
125/// # Safety
126/// This type is send because it only contains a pointer to a GraalVM isolate, which can be safely sent across threads as long as the GraalVM API is used correctly (i.e., attaching/detaching threads properly).
127/// The actual thread management and isolate usage are handled in the `ChocoBackend` struct, which ensures that threads are attached/detached correctly when interacting with the GraalVM environment.
128unsafe impl Send for IsolatePtr {}
129static ISOLATE_PTR: Mutex<IsolatePtr> = Mutex::new(IsolatePtr(None));
130
131thread_local! {
132    pub(crate) static CHOCO_BACKEND: ChocoBackend = {
133        // # Safety:
134        // Initializes the GraalVM isolate if not yet done.
135        // This is required before any interaction with the Choco backend.
136        // It is safe to call this function multiple times from the same thread, as it will only initialize once.
137        unsafe {
138            let mut thread: *mut graal_isolatethread_t = ptr::null_mut();
139
140            let mut lock = ISOLATE_PTR
141                .lock()
142                .expect("Failed to acquire lock for creating GraalVM isolate");
143            match lock.0 {
144                None => {
145                    let mut isolate: *mut graal_isolate_t = ptr::null_mut();
146                    if CHOCO_LIB.graal_create_isolate(ptr::null_mut(), &mut isolate, &mut thread)
147                        != 0
148                    {
149                        panic!("graal_create_isolate error");
150                    }
151                    lock.0 = Some(isolate);
152                }
153
154                Some(isolate_ptr) => {
155                    if CHOCO_LIB.graal_attach_thread(isolate_ptr, &mut thread) != 0 {
156                        panic!("graal_create_isolate error");
157                    }
158                }
159            }
160            ChocoBackend { thread }
161}}}
162
163/// Errors that can occur during constraint solving.
164#[derive(Error, Debug, PartialEq, Eq)]
165#[non_exhaustive]
166pub enum SolverError {
167    #[error("Tried to post a not free constraint or reified constraint")]
168    NotFreeConstraint,
169    #[error("Found contradiction while propagating")]
170    FoundContradiction,
171    #[error("Cannot be converted to BoolVar: domain is not [0,1]")]
172    BoolVarConversionError,
173    #[error("unknown data store error")]
174    Unknown,
175}
176
177/// Either an integer constant or an integer variable reference.
178///
179/// Used to allow functions to accept either a constant value or a variable.
180pub(crate) enum IntOrIntVar<'a, 'model> {
181    /// An integer variable.
182    IntVar(&'a IntVar<'model>),
183    /// An integer constant.
184    Int(i32),
185}
186
187impl<'a, 'model: 'a> From<&'a IntVar<'model>> for IntOrIntVar<'a, 'model> {
188    fn from(val: &'a IntVar<'model>) -> Self {
189        IntOrIntVar::IntVar(val)
190    }
191}
192impl From<i32> for IntOrIntVar<'_, '_> {
193    fn from(val: i32) -> Self {
194        IntOrIntVar::Int(val)
195    }
196}
197
198trait Sealed {}
199impl Sealed for BoolVar<'_> {}
200impl Sealed for IntVar<'_> {}
201impl Sealed for Constraint<'_> {}
202impl<Q> Sealed for &Q {}
203impl<Q: Sealed> Sealed for &[Q] {}
204
205// :TODO: Unsafe code isolate in backend need to be sincronized
206pub struct ChocoBackend {
207    thread: *mut graal_isolatethread_t,
208}
209
210impl ChocoBackend {
211    /// Sets the folder path where the DLL files are located.
212    /// # Arguments
213    ///
214    /// * `dll_folder_path` - A `String` representing the path to the folder containing the DLL files.
215    ///
216    /// # Example
217    ///
218    /// ```no_run
219    /// use choco_solver::ChocoBackend;
220    /// ChocoBackend::set_dll_folder(String::from("C:/path/to/dll/folder"));
221    /// ```
222    ///
223    /// # Note
224    ///
225    /// This function should be called before any interaction with the Choco backend,
226    /// as it configures the directory from which the `libchoco_capi` library will be loaded.
227    /// If not set, the library will be searched in folder specified by environment variable `CHOCO_SOLVER_DLL_FOLDER`.
228    /// If the environment variable is not set, the library will be searched in the default system paths.
229    pub fn set_dll_folder(dll_folder_path: String) -> Result<(), Option<String>> {
230        DLL_FOLDER.set(Some(dll_folder_path))
231    }
232}
233
234impl Drop for ChocoBackend {
235    fn drop(&mut self) {
236        // # Safety:
237        // Detach the thread from the GraalVM isolate when the backend is dropped. This is necessary to clean up resources associated with the thread in the GraalVM environment.
238        //It is safe to call this function as part of the drop process, as it will only detach the thread if it was successfully attached during initialization.
239        unsafe {
240            CHOCO_LIB.graal_detach_thread(self.thread);
241        }
242    }
243}
244
245#[allow(private_bounds)]
246/// Trait for creating global constraints over arrays of variables.
247///
248/// Provides methods for constraints that operate on collections of variables,
249/// such as all-different, all-equal, and cardinality constraints.
250pub trait ArrayEqualityConstraints<'model>: Sealed {
251    ///  Creates an allDifferent constraint, which ensures that all variables from vars take a different value.    /// # Panic:
252    /// if slice is empty
253    /// # Panic:
254    /// if slice is empty
255    fn all_different(self) -> Constraint<'model>;
256    ///  Creates an allDifferent constraint for variables that are not equal to 0.
257    ///  There can be multiple variables equal to 0.
258    /// # Panic:
259    /// if slice is empty
260    fn all_different_except_0(self) -> Constraint<'model>;
261    /// Creates an all_equal constraint.
262    /// Ensures that all variables from vars take the same value.
263    /// # Panic:
264    /// if slice is empty
265    fn all_equal(self) -> Constraint<'model>;
266    /// Creates a not_all_equal constraint.
267    /// Ensures that not all variables from vars take the same value.
268    /// # Panic:
269    /// if slice is empty
270    fn not_all_equal(self) -> Constraint<'model>;
271    /// Creates an at_least_n_value constraint.
272    /// Let N be the number of distinct values assigned to the variables of the intvars collection.
273    /// Enforce condition N >= n_values to hold.
274    /// This embeds a light propagator by default.
275    /// Additional filtering algorithms can be added.
276    /// # Arguments
277    /// * `n_values` - IntVar (limit variable).
278    /// * `ac` - If True, add additional filtering algorithm, domain filtering algorithm derivated
279    ///   from (Soft) AllDifferent.
280    /// # Panic:
281    /// if slice is empty
282    fn at_least_n_value<'a>(self, n_values: &'a IntVar<'model>, ac: bool) -> Constraint<'model>
283    where
284        'model: 'a;
285    ///  Creates an at_mostn_value constraint.
286    ///  Let N be the number of distinct values assigned to the variables of the intvars collection.
287    ///  Enforce condition N <= n_values to hold.
288    ///  This embeds a light propagator by default. Additional filtering algorithms can be added.
289    ///  # Arguments
290    ///  * `n_values` - IntVar (limit variable).
291    ///  * `strong` - "AMNV<Gci | MDRk | R13>" Filters the conjunction of AtMostNValue and inequalities
292    ///    (see Fages and Lap&egrave;gue Artificial Intelligence 2014)
293    ///    automatically detects inequalities and allDifferent constraints.
294    ///    Presumably useful when nValues must be minimized.
295    fn at_most_n_value<'a>(self, n_values: &IntVar<'model>, strong: bool) -> Constraint<'model>
296    where
297        'model: 'a;
298}