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è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}