1use std::collections::HashMap;
21use std::ffi::{CStr, CString, c_char, c_int, c_long, c_void};
22use std::path::Path;
23use std::ptr;
24use std::sync::{Arc, Mutex, OnceLock};
25
26#[cfg(any(unix, windows))]
27use libloading::{Library, Symbol};
28
29use crate::nl_reader::{Expr, FuncallArg, ImportedFunc};
30
31#[derive(Default, Clone)]
37pub struct ExternalResolver {
38 pub funcs_by_id: HashMap<usize, (Arc<ExternalLibrary>, String)>,
40}
41
42impl ExternalResolver {
43 pub fn is_empty(&self) -> bool {
44 self.funcs_by_id.is_empty()
45 }
46
47 pub fn build_for_problem(
56 imported_funcs: &[ImportedFunc],
57 referenced_ids: &std::collections::BTreeSet<usize>,
58 ) -> Result<Self, String> {
59 if referenced_ids.is_empty() {
60 return Ok(Self::default());
61 }
62 let amplfunc = std::env::var("AMPLFUNC").map_err(|_| {
63 "problem uses external functions but AMPLFUNC is not set; \
64 set AMPLFUNC to a newline-separated list of AMPL shared-library paths"
65 .to_string()
66 })?;
67 let mut libs: Vec<Arc<ExternalLibrary>> = Vec::new();
68 for path_str in amplfunc
69 .split('\n')
70 .map(|s| s.trim())
71 .filter(|s| !s.is_empty())
72 {
73 let path = std::path::Path::new(path_str);
74 let lib = ExternalLibrary::load(path).map_err(|e| format!("AMPLFUNC: {e}"))?;
75 libs.push(Arc::new(lib));
76 }
77
78 let mut funcs_by_id: HashMap<usize, (Arc<ExternalLibrary>, String)> = HashMap::new();
79 for id in referenced_ids {
80 let decl = imported_funcs
81 .iter()
82 .find(|f| f.id == *id)
83 .ok_or_else(|| format!("funcall id {id} has no F<{id}> declaration"))?;
84 let found = libs
85 .iter()
86 .find(|lib| lib.get(&decl.name).is_some())
87 .ok_or_else(|| {
88 format!(
89 "external function '{}' (id {}) not found in any library on AMPLFUNC",
90 decl.name, decl.id
91 )
92 })?;
93 funcs_by_id.insert(*id, (found.clone(), decl.name.clone()));
94 }
95 Ok(Self { funcs_by_id })
96 }
97}
98
99pub fn collect_funcall_ids(e: &Expr, out: &mut std::collections::BTreeSet<usize>) {
103 let mut seen: std::collections::HashSet<*const Expr> = std::collections::HashSet::new();
111 collect_funcall_ids_memo(e, out, &mut seen);
112}
113
114fn collect_funcall_ids_memo(
115 e: &Expr,
116 out: &mut std::collections::BTreeSet<usize>,
117 seen: &mut std::collections::HashSet<*const Expr>,
118) {
119 match e {
120 Expr::Const(_) | Expr::Var(_) => {}
121 Expr::Binary(_, a, b) => {
122 collect_funcall_ids_memo(a, out, seen);
123 collect_funcall_ids_memo(b, out, seen);
124 }
125 Expr::Unary(_, a) => collect_funcall_ids_memo(a, out, seen),
126 Expr::Sum(args) | Expr::MinList(args) | Expr::MaxList(args) => {
127 for a in args {
128 collect_funcall_ids_memo(a, out, seen);
129 }
130 }
131 Expr::Compare(_, a, b) | Expr::And(a, b) | Expr::Or(a, b) => {
132 collect_funcall_ids_memo(a, out, seen);
133 collect_funcall_ids_memo(b, out, seen);
134 }
135 Expr::Not(a) => collect_funcall_ids_memo(a, out, seen),
136 Expr::Cond { cond, then_, else_ } => {
137 collect_funcall_ids_memo(cond, out, seen);
138 collect_funcall_ids_memo(then_, out, seen);
139 collect_funcall_ids_memo(else_, out, seen);
140 }
141 Expr::Cse(body) => {
142 if seen.insert(std::sync::Arc::as_ptr(body)) {
143 collect_funcall_ids_memo(body, out, seen);
144 }
145 }
146 Expr::Funcall { id, args } => {
147 out.insert(*id);
148 for arg in args {
149 if let FuncallArg::Real(e) = arg {
150 collect_funcall_ids_memo(e, out, seen);
151 }
152 }
153 }
154 }
155}
156
157fn ampl_lock() -> &'static Mutex<()> {
163 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
164 LOCK.get_or_init(|| Mutex::new(()))
165}
166
167pub const FUNCADD_REAL_VALUED: i32 = 0;
169pub const FUNCADD_STRING_ARGS: i32 = 1;
171pub const FUNCADD_OUTPUT_ARGS: i32 = 2;
173pub const FUNCADD_RANDOM_VALUED: i32 = 4;
174
175#[repr(C)]
177pub struct Arglist {
178 pub n: c_int, pub nr: c_int, pub at: *mut c_int, pub ra: *mut f64, pub sa: *mut *const c_char, pub derivs: *mut f64, pub hes: *mut f64, pub dig: *mut c_char, pub funcinfo: *mut c_void, pub ae: *mut AmplExports, pub f: *mut c_void, pub tva: *mut c_void, pub errmsg: *mut c_char, pub tmi: *mut c_void, pub private: *mut c_char,
193 pub nin: c_int,
194 pub nout: c_int,
195 pub nsin: c_int,
196 pub nsout: c_int,
197}
198
199pub type Rfunc = unsafe extern "C" fn(*mut Arglist) -> f64;
202
203pub type AddfuncFn = unsafe extern "C" fn(
205 name: *const c_char,
206 f: Rfunc,
207 ty: c_int,
208 nargs: c_int,
209 funcinfo: *mut c_void,
210 ae: *mut AmplExports,
211);
212
213pub type RandSeedSetter = unsafe extern "C" fn(*mut c_void, std::os::raw::c_ulong);
215
216pub type AddrandinitFn =
218 unsafe extern "C" fn(ae: *mut AmplExports, setter: RandSeedSetter, v: *mut c_void);
219
220pub type AtResetFn = unsafe extern "C" fn(ae: *mut AmplExports, f: *mut c_void, v: *mut c_void);
222
223#[repr(C)]
228pub struct AmplExports {
229 pub std_err: *mut c_void,
230 pub addfunc: Option<AddfuncFn>,
231 pub asl_date: c_long,
232 pub fprintf: *mut c_void,
233 pub printf: *mut c_void,
234 pub sprintf: *mut c_void,
235 pub vfprintf: *mut c_void,
236 pub vsprintf: *mut c_void,
237 pub strtod: *mut c_void,
238 pub crypto: *mut c_void,
239 pub asl: *mut c_char,
240 pub at_exit: *mut c_void,
241 pub at_reset: Option<AtResetFn>,
242 pub tempmem: *mut c_void,
243 pub add_table_handler: *mut c_void,
244 pub private_ae: *mut c_char,
245 pub qsortv: *mut c_void,
246
247 pub std_in: *mut c_void,
248 pub std_out: *mut c_void,
249 pub clearerr: *mut c_void,
250 pub fclose: *mut c_void,
251 pub fdopen: *mut c_void,
252 pub feof: *mut c_void,
253 pub ferror: *mut c_void,
254 pub fflush: *mut c_void,
255 pub fgetc: *mut c_void,
256 pub fgets: *mut c_void,
257 pub fileno: *mut c_void,
258 pub fopen: *mut c_void,
259 pub fputc: *mut c_void,
260 pub fputs: *mut c_void,
261 pub fread: *mut c_void,
262 pub freopen: *mut c_void,
263 pub fscanf: *mut c_void,
264 pub fseek: *mut c_void,
265 pub ftell: *mut c_void,
266 pub fwrite: *mut c_void,
267 pub pclose: *mut c_void,
268 pub perror: *mut c_void,
269 pub popen: *mut c_void,
270 pub puts: *mut c_void,
271 pub rewind: *mut c_void,
272 pub scanf: *mut c_void,
273 pub setbuf: *mut c_void,
274 pub setvbuf: *mut c_void,
275 pub sscanf: *mut c_void,
276 pub tempnam: *mut c_void,
277 pub tmpfile: *mut c_void,
278 pub tmpnam: *mut c_void,
279 pub ungetc: *mut c_void,
280 pub ai: *mut c_void,
281 pub getenv: *mut c_void,
282 pub breakfunc: *mut c_void,
283 pub breakarg: *mut c_char,
284
285 pub snprintf: *mut c_void,
287 pub vsnprintf: *mut c_void,
288
289 pub addrand: *mut c_void,
290 pub addrandinit: Option<AddrandinitFn>,
291}
292
293unsafe impl Send for AmplExports {}
299unsafe impl Sync for AmplExports {}
300
301#[derive(Debug, Clone)]
304pub struct RegisteredFunc {
305 pub name: String,
306 pub rfunc: Rfunc,
307 pub ty: i32,
309 pub nargs: i32,
312 pub funcinfo: *mut c_void,
314}
315
316unsafe impl Send for RegisteredFunc {}
320unsafe impl Sync for RegisteredFunc {}
321
322impl std::fmt::Debug for ExternalLibrary {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 f.debug_struct("ExternalLibrary")
325 .field("funcs", &self.funcs.keys().collect::<Vec<_>>())
326 .finish()
327 }
328}
329
330pub struct ExternalLibrary {
332 #[cfg(any(unix, windows))]
337 _lib: Arc<Library>,
338 _ae: Box<AmplExports>,
342 funcs: HashMap<String, RegisteredFunc>,
344}
345
346impl ExternalLibrary {
347 #[cfg(not(any(unix, windows)))]
353 pub fn load(path: &Path) -> Result<Self, String> {
354 Err(format!(
355 "cannot load '{}': AMPL imported (external) functions need a \
356 dynamic library loader, which this target does not have",
357 path.display()
358 ))
359 }
360
361 #[cfg(any(unix, windows))]
364 pub fn load(path: &Path) -> Result<Self, String> {
365 let _guard = ampl_lock().lock().unwrap_or_else(|e| e.into_inner());
368 let lib = unsafe { Library::new(path) }
372 .map_err(|e| format!("failed to open '{}': {}", path.display(), e))?;
373
374 type FuncaddFn = unsafe extern "C" fn(*mut AmplExports);
377 let funcadd: Symbol<FuncaddFn> = unsafe { lib.get(b"funcadd_ASL\0") }
378 .map_err(|e| format!("no funcadd_ASL in '{}': {}", path.display(), e))?;
379
380 let mut ae = Box::new(AmplExports {
384 std_err: ptr::null_mut(),
385 addfunc: Some(trampoline_addfunc),
386 asl_date: 20160307,
389 fprintf: ptr::null_mut(),
390 printf: ptr::null_mut(),
391 sprintf: ptr::null_mut(),
392 vfprintf: ptr::null_mut(),
393 vsprintf: ptr::null_mut(),
394 strtod: ptr::null_mut(),
395 crypto: ptr::null_mut(),
396 asl: ptr::null_mut(),
397 at_exit: ptr::null_mut(),
398 at_reset: Some(trampoline_atreset),
399 tempmem: ptr::null_mut(),
400 add_table_handler: ptr::null_mut(),
401 private_ae: ptr::null_mut(),
402 qsortv: ptr::null_mut(),
403 std_in: ptr::null_mut(),
404 std_out: ptr::null_mut(),
405 clearerr: ptr::null_mut(),
406 fclose: ptr::null_mut(),
407 fdopen: ptr::null_mut(),
408 feof: ptr::null_mut(),
409 ferror: ptr::null_mut(),
410 fflush: ptr::null_mut(),
411 fgetc: ptr::null_mut(),
412 fgets: ptr::null_mut(),
413 fileno: ptr::null_mut(),
414 fopen: ptr::null_mut(),
415 fputc: ptr::null_mut(),
416 fputs: ptr::null_mut(),
417 fread: ptr::null_mut(),
418 freopen: ptr::null_mut(),
419 fscanf: ptr::null_mut(),
420 fseek: ptr::null_mut(),
421 ftell: ptr::null_mut(),
422 fwrite: ptr::null_mut(),
423 pclose: ptr::null_mut(),
424 perror: ptr::null_mut(),
425 popen: ptr::null_mut(),
426 puts: ptr::null_mut(),
427 rewind: ptr::null_mut(),
428 scanf: ptr::null_mut(),
429 setbuf: ptr::null_mut(),
430 setvbuf: ptr::null_mut(),
431 sscanf: ptr::null_mut(),
432 tempnam: ptr::null_mut(),
433 tmpfile: ptr::null_mut(),
434 tmpnam: ptr::null_mut(),
435 ungetc: ptr::null_mut(),
436 ai: ptr::null_mut(),
437 getenv: ptr::null_mut(),
438 breakfunc: ptr::null_mut(),
439 breakarg: ptr::null_mut(),
440 snprintf: ptr::null_mut(),
441 vsnprintf: ptr::null_mut(),
442 addrand: ptr::null_mut(),
443 addrandinit: Some(trampoline_addrandinit),
444 });
445
446 REGISTRY_SINK.with(|sink| {
449 let mut guard = sink.borrow_mut();
450 assert!(
451 guard.is_none(),
452 "nested ExternalLibrary::load is not supported"
453 );
454 *guard = Some(HashMap::new());
455 });
456
457 unsafe { funcadd(ae.as_mut()) };
460
461 let funcs = REGISTRY_SINK
462 .with(|sink| sink.borrow_mut().take())
463 .unwrap_or_default();
464
465 Ok(ExternalLibrary {
466 _lib: Arc::new(lib),
467 _ae: ae,
468 funcs,
469 })
470 }
471
472 pub fn function_names(&self) -> impl Iterator<Item = &str> {
474 self.funcs.keys().map(|s| s.as_str())
475 }
476
477 pub fn get(&self, name: &str) -> Option<&RegisteredFunc> {
479 self.funcs.get(name)
480 }
481
482 pub fn eval(
494 &self,
495 name: &str,
496 args: &[ExternalArg<'_>],
497 want_derivs: bool,
498 want_hes: bool,
499 ) -> Result<EvalResult, String> {
500 let rf = self
501 .funcs
502 .get(name)
503 .ok_or_else(|| format!("no such external function '{name}'"))?;
504
505 let n = args.len() as i32;
507 if rf.nargs >= 0 {
508 if rf.nargs != n {
509 return Err(format!(
510 "external '{name}' expects {} args, got {}",
511 rf.nargs, n
512 ));
513 }
514 } else {
515 let min_args = -(rf.nargs + 1);
517 if n < min_args {
518 return Err(format!(
519 "external '{name}' expects at least {min_args} args, got {n}"
520 ));
521 }
522 }
523
524 let mut at_vec: Vec<c_int> = Vec::with_capacity(args.len());
526 let mut ra_vec: Vec<f64> = Vec::new();
527 let mut sa_owned: Vec<CString> = Vec::new();
528 for a in args {
529 match a {
530 ExternalArg::Real(x) => {
531 at_vec.push(ra_vec.len() as c_int);
532 ra_vec.push(*x);
533 }
534 ExternalArg::Str(s) => {
535 let cs = CString::new(*s)
536 .map_err(|_| format!("external '{name}' string arg contains NUL"))?;
537 at_vec.push(-(sa_owned.len() as c_int + 1));
538 sa_owned.push(cs);
539 }
540 }
541 }
542 let nr = ra_vec.len() as c_int;
543 let sa_ptrs: Vec<*const c_char> = sa_owned.iter().map(|s| s.as_ptr()).collect();
544
545 let has_strings = !sa_owned.is_empty();
548 if has_strings && (rf.ty & FUNCADD_STRING_ARGS) == 0 {
549 return Err(format!(
550 "external '{name}' is not declared FUNCADD_STRING_ARGS but was \
551 called with string arguments"
552 ));
553 }
554
555 let mut derivs_buf: Vec<f64> = if want_derivs {
557 vec![0.0; nr as usize]
558 } else {
559 Vec::new()
560 };
561 let hes_len = if want_hes {
562 (nr as usize) * ((nr as usize) + 1) / 2
563 } else {
564 0
565 };
566 let mut hes_buf: Vec<f64> = if want_hes {
567 vec![0.0; hes_len]
568 } else {
569 Vec::new()
570 };
571
572 let mut errmsg_buf: Vec<c_char> = vec![0; 1024];
579 let errmsg_orig_ptr = errmsg_buf.as_ptr();
580
581 let mut al = Arglist {
585 n,
586 nr,
587 at: if at_vec.is_empty() {
588 ptr::null_mut()
589 } else {
590 at_vec.as_mut_ptr()
591 },
592 ra: if ra_vec.is_empty() {
593 ptr::null_mut()
594 } else {
595 ra_vec.as_mut_ptr()
596 },
597 sa: if sa_ptrs.is_empty() {
598 ptr::null_mut()
599 } else {
600 sa_ptrs.as_ptr() as *mut *const c_char
601 },
602 derivs: if want_derivs {
603 derivs_buf.as_mut_ptr()
604 } else {
605 ptr::null_mut()
606 },
607 hes: if want_hes {
608 hes_buf.as_mut_ptr()
609 } else {
610 ptr::null_mut()
611 },
612 dig: ptr::null_mut(),
613 funcinfo: rf.funcinfo,
614 ae: self._ae_ptr(),
617 f: ptr::null_mut(),
618 tva: ptr::null_mut(),
619 errmsg: errmsg_buf.as_mut_ptr(),
620 tmi: ptr::null_mut(),
621 private: ptr::null_mut(),
622 nin: 0,
623 nout: 0,
624 nsin: 0,
625 nsout: 0,
626 };
627
628 let _guard = ampl_lock().lock().unwrap_or_else(|e| e.into_inner());
632 let value = unsafe { (rf.rfunc)(&mut al as *mut Arglist) };
633 drop(_guard);
634
635 if let Some(msg) =
642 unsafe { decode_external_errmsg(al.errmsg, errmsg_orig_ptr, errmsg_buf[0]) }
643 {
644 return Err(format!("external '{name}' reported: {msg}"));
645 }
646
647 Ok(EvalResult {
648 value,
649 derivs: if want_derivs { Some(derivs_buf) } else { None },
650 hessian: if want_hes { Some(hes_buf) } else { None },
651 })
652 }
653
654 fn _ae_ptr(&self) -> *mut AmplExports {
658 (&*self._ae as *const AmplExports) as *mut AmplExports
660 }
661}
662
663#[derive(Debug, Clone, Copy)]
665pub enum ExternalArg<'a> {
666 Real(f64),
667 Str(&'a str),
668}
669
670#[derive(Debug, Clone)]
672pub struct EvalResult {
673 pub value: f64,
675 pub derivs: Option<Vec<f64>>,
677 pub hessian: Option<Vec<f64>>,
680}
681
682unsafe fn decode_external_errmsg(
705 errmsg_field: *const c_char,
706 orig_buf_ptr: *const c_char,
707 buf_first: c_char,
708) -> Option<String> {
709 if !errmsg_field.is_null() && errmsg_field != orig_buf_ptr {
710 return Some(
713 unsafe { CStr::from_ptr(errmsg_field) }
714 .to_string_lossy()
715 .into_owned(),
716 );
717 }
718 if buf_first != 0 {
719 return Some(
722 unsafe { CStr::from_ptr(orig_buf_ptr) }
723 .to_string_lossy()
724 .into_owned(),
725 );
726 }
727 None
728}
729
730thread_local! {
740 static REGISTRY_SINK: std::cell::RefCell<Option<HashMap<String, RegisteredFunc>>> =
741 std::cell::RefCell::new(None);
742}
743
744#[cfg(any(unix, windows))]
746unsafe extern "C" fn trampoline_addfunc(
747 name: *const c_char,
748 f: Rfunc,
749 ty: c_int,
750 nargs: c_int,
751 funcinfo: *mut c_void,
752 _ae: *mut AmplExports,
753) {
754 if name.is_null() {
755 return;
756 }
757 let cname = unsafe { CStr::from_ptr(name) };
759 let name_str = match cname.to_str() {
760 Ok(s) => s.to_owned(),
761 Err(_) => return, };
763 REGISTRY_SINK.with(|sink| {
764 if let Some(map) = sink.borrow_mut().as_mut() {
765 map.insert(
766 name_str.clone(),
767 RegisteredFunc {
768 name: name_str,
769 rfunc: f,
770 ty: ty as i32,
771 nargs: nargs as i32,
772 funcinfo,
773 },
774 );
775 }
776 });
777}
778
779#[cfg(any(unix, windows))]
782unsafe extern "C" fn trampoline_atreset(_ae: *mut AmplExports, _f: *mut c_void, _v: *mut c_void) {
783 tracing::debug!("external library registered an AtReset callback; ignoring");
784}
785
786#[cfg(any(unix, windows))]
789unsafe extern "C" fn trampoline_addrandinit(
790 _ae: *mut AmplExports,
791 setter: RandSeedSetter,
792 v: *mut c_void,
793) {
794 unsafe { setter(v, 1) };
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800
801 fn idaes_dylib() -> Option<std::path::PathBuf> {
802 let home = std::env::var_os("HOME")?;
803 let p = std::path::PathBuf::from(home).join(".idaes/bin/general_helmholtz_external.dylib");
804 if p.exists() { Some(p) } else { None }
805 }
806
807 fn idaes_params_dir() -> Option<String> {
808 let home = std::env::var_os("HOME")?;
809 let p = std::path::PathBuf::from(home).join(
810 "Dropbox/uv/.venv/lib/python3.12/site-packages/idaes/\
811 models/properties/general_helmholtz/components/parameters/",
812 );
813 if p.exists() {
814 p.to_str().map(|s| s.to_owned())
815 } else {
816 None
817 }
818 }
819
820 #[test]
823 fn load_idaes_helmholtz_dylib_registers_known_functions() {
824 let Some(path) = idaes_dylib() else {
825 eprintln!("skipping: IDAES dylib not present");
826 return;
827 };
828
829 let lib = ExternalLibrary::load(&path).expect("load should succeed");
830 let names: Vec<String> = lib.function_names().map(|s| s.to_owned()).collect();
831
832 for required in &["vf_hp", "h_liq_hp", "h_vap_hp"] {
833 assert!(
834 names.iter().any(|n| n == required),
835 "expected {required} in registered names: {names:?}"
836 );
837 }
838 }
839
840 #[test]
844 fn eval_vf_hp_at_fixture_initial_point() {
845 let Some(path) = idaes_dylib() else {
846 eprintln!("skipping: IDAES dylib not present");
847 return;
848 };
849 let Some(params_dir) = idaes_params_dir() else {
850 eprintln!("skipping: IDAES parameters directory not present");
851 return;
852 };
853
854 let lib = ExternalLibrary::load(&path).expect("load");
855 let args = [
860 ExternalArg::Str("h2o"),
861 ExternalArg::Real(1878.71 * 0.055508472036052976),
862 ExternalArg::Real(101325.0 * 0.001),
863 ExternalArg::Str(¶ms_dir),
864 ];
865 let res = lib.eval("vf_hp", &args, false, false).expect("eval");
866 assert!(
867 res.value.is_finite(),
868 "vf_hp returned non-finite value {}",
869 res.value
870 );
871 }
872
873 #[test]
876 fn eval_vf_hp_with_derivatives() {
877 let Some(path) = idaes_dylib() else {
878 eprintln!("skipping: IDAES dylib not present");
879 return;
880 };
881 let Some(params_dir) = idaes_params_dir() else {
882 eprintln!("skipping: IDAES parameters directory not present");
883 return;
884 };
885
886 let lib = ExternalLibrary::load(&path).expect("load");
887 let args = [
888 ExternalArg::Str("h2o"),
889 ExternalArg::Real(1878.71 * 0.055508472036052976),
890 ExternalArg::Real(101325.0 * 0.001),
891 ExternalArg::Str(¶ms_dir),
892 ];
893 let res = lib.eval("vf_hp", &args, true, false).expect("eval");
894 let derivs = res.derivs.expect("derivs requested");
895 assert_eq!(derivs.len(), 2, "nr=2 reals -> 2 derivatives");
896 for (i, d) in derivs.iter().enumerate() {
897 assert!(d.is_finite(), "derivs[{i}] = {d} not finite");
898 }
899 }
900
901 #[test]
904 fn eval_vf_hp_with_hessian() {
905 let Some(path) = idaes_dylib() else {
906 eprintln!("skipping: IDAES dylib not present");
907 return;
908 };
909 let Some(params_dir) = idaes_params_dir() else {
910 eprintln!("skipping: IDAES parameters directory not present");
911 return;
912 };
913
914 let lib = ExternalLibrary::load(&path).expect("load");
915 let args = [
916 ExternalArg::Str("h2o"),
917 ExternalArg::Real(1878.71 * 0.055508472036052976),
918 ExternalArg::Real(101325.0 * 0.001),
919 ExternalArg::Str(¶ms_dir),
920 ];
921 let res = lib.eval("vf_hp", &args, true, true).expect("eval");
922 let hes = res.hessian.expect("hessian requested");
923 assert_eq!(hes.len(), 3, "nr=2 -> packed Hessian of length 3");
924 for (i, h) in hes.iter().enumerate() {
925 assert!(h.is_finite(), "hes[{i}] = {h} not finite");
926 }
927 }
928
929 unsafe extern "C" fn rfunc_reassigns_errmsg(al: *mut Arglist) -> f64 {
936 static MSG: &[u8] = b"T out of range\0";
937 unsafe {
939 (*al).errmsg = MSG.as_ptr() as *mut c_char;
940 }
941 f64::NAN
942 }
943
944 fn null_arglist(errmsg: *mut c_char) -> Arglist {
947 Arglist {
948 n: 1,
949 nr: 1,
950 at: ptr::null_mut(),
951 ra: ptr::null_mut(),
952 sa: ptr::null_mut(),
953 derivs: ptr::null_mut(),
954 hes: ptr::null_mut(),
955 dig: ptr::null_mut(),
956 funcinfo: ptr::null_mut(),
957 ae: ptr::null_mut(),
958 f: ptr::null_mut(),
959 tva: ptr::null_mut(),
960 errmsg,
961 tmi: ptr::null_mut(),
962 private: ptr::null_mut(),
963 nin: 0,
964 nout: 0,
965 nsin: 0,
966 nsout: 0,
967 }
968 }
969
970 #[test]
976 fn reassigned_errmsg_pointer_is_detected_end_to_end() {
977 let mut errmsg_buf: Vec<c_char> = vec![0; 1024];
978 let orig_ptr = errmsg_buf.as_ptr();
979 let mut al = null_arglist(errmsg_buf.as_mut_ptr());
980
981 let v = unsafe { rfunc_reassigns_errmsg(&mut al) };
983 assert!(v.is_nan(), "the failing eval returned NaN");
984
985 assert_eq!(
988 errmsg_buf[0], 0,
989 "a reassigning library must not touch the caller buffer"
990 );
991
992 let decoded = unsafe { decode_external_errmsg(al.errmsg, orig_ptr, errmsg_buf[0]) };
994 assert_eq!(
995 decoded.as_deref(),
996 Some("T out of range"),
997 "the reassigned errmsg pointer must be surfaced as an error"
998 );
999 }
1000
1001 #[test]
1004 fn decode_external_errmsg_buffer_and_none_channels() {
1005 let mut buf: Vec<c_char> = vec![0; 16];
1007 for (i, b) in b"bad input".iter().enumerate() {
1008 buf[i] = *b as c_char;
1009 }
1010 let orig = buf.as_ptr();
1011 let decoded = unsafe { decode_external_errmsg(orig, orig, buf[0]) };
1012 assert_eq!(decoded.as_deref(), Some("bad input"));
1013
1014 let zero: Vec<c_char> = vec![0; 16];
1016 let z = zero.as_ptr();
1017 assert_eq!(unsafe { decode_external_errmsg(z, z, zero[0]) }, None);
1018
1019 assert_eq!(unsafe { decode_external_errmsg(ptr::null(), z, 0) }, None);
1021 }
1022}