1use std::collections::HashMap;
21use std::ffi::{c_char, c_int, c_long, c_void, CStr, CString};
22use std::path::Path;
23use std::ptr;
24use std::sync::{Arc, Mutex, OnceLock};
25
26use libloading::{Library, Symbol};
27
28use crate::nl_reader::{Expr, FuncallArg, ImportedFunc};
29
30#[derive(Default, Clone)]
36pub struct ExternalResolver {
37 pub funcs_by_id: HashMap<usize, (Arc<ExternalLibrary>, String)>,
39}
40
41impl ExternalResolver {
42 pub fn is_empty(&self) -> bool {
43 self.funcs_by_id.is_empty()
44 }
45
46 pub fn build_for_problem(
55 imported_funcs: &[ImportedFunc],
56 referenced_ids: &std::collections::BTreeSet<usize>,
57 ) -> Result<Self, String> {
58 if referenced_ids.is_empty() {
59 return Ok(Self::default());
60 }
61 let amplfunc = std::env::var("AMPLFUNC").map_err(|_| {
62 "problem uses external functions but AMPLFUNC is not set; \
63 set AMPLFUNC to a newline-separated list of AMPL shared-library paths"
64 .to_string()
65 })?;
66 let mut libs: Vec<Arc<ExternalLibrary>> = Vec::new();
67 for path_str in amplfunc
68 .split('\n')
69 .map(|s| s.trim())
70 .filter(|s| !s.is_empty())
71 {
72 let path = std::path::Path::new(path_str);
73 let lib = ExternalLibrary::load(path).map_err(|e| format!("AMPLFUNC: {e}"))?;
74 libs.push(Arc::new(lib));
75 }
76
77 let mut funcs_by_id: HashMap<usize, (Arc<ExternalLibrary>, String)> = HashMap::new();
78 for id in referenced_ids {
79 let decl = imported_funcs
80 .iter()
81 .find(|f| f.id == *id)
82 .ok_or_else(|| format!("funcall id {id} has no F<{id}> declaration"))?;
83 let found = libs
84 .iter()
85 .find(|lib| lib.get(&decl.name).is_some())
86 .ok_or_else(|| {
87 format!(
88 "external function '{}' (id {}) not found in any library on AMPLFUNC",
89 decl.name, decl.id
90 )
91 })?;
92 funcs_by_id.insert(*id, (found.clone(), decl.name.clone()));
93 }
94 Ok(Self { funcs_by_id })
95 }
96}
97
98pub fn collect_funcall_ids(e: &Expr, out: &mut std::collections::BTreeSet<usize>) {
102 match e {
103 Expr::Const(_) | Expr::Var(_) => {}
104 Expr::Binary(_, a, b) => {
105 collect_funcall_ids(a, out);
106 collect_funcall_ids(b, out);
107 }
108 Expr::Unary(_, a) => collect_funcall_ids(a, out),
109 Expr::Sum(args) | Expr::MinList(args) | Expr::MaxList(args) => {
110 for a in args {
111 collect_funcall_ids(a, out);
112 }
113 }
114 Expr::Compare(_, a, b) | Expr::And(a, b) | Expr::Or(a, b) => {
115 collect_funcall_ids(a, out);
116 collect_funcall_ids(b, out);
117 }
118 Expr::Not(a) => collect_funcall_ids(a, out),
119 Expr::Cond { cond, then_, else_ } => {
120 collect_funcall_ids(cond, out);
121 collect_funcall_ids(then_, out);
122 collect_funcall_ids(else_, out);
123 }
124 Expr::Cse(body) => collect_funcall_ids(body, out),
125 Expr::Funcall { id, args } => {
126 out.insert(*id);
127 for arg in args {
128 if let FuncallArg::Real(e) = arg {
129 collect_funcall_ids(e, out);
130 }
131 }
132 }
133 }
134}
135
136fn ampl_lock() -> &'static Mutex<()> {
142 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
143 LOCK.get_or_init(|| Mutex::new(()))
144}
145
146pub const FUNCADD_REAL_VALUED: i32 = 0;
148pub const FUNCADD_STRING_ARGS: i32 = 1;
150pub const FUNCADD_OUTPUT_ARGS: i32 = 2;
152pub const FUNCADD_RANDOM_VALUED: i32 = 4;
153
154#[repr(C)]
156pub struct Arglist {
157 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,
172 pub nin: c_int,
173 pub nout: c_int,
174 pub nsin: c_int,
175 pub nsout: c_int,
176}
177
178pub type Rfunc = unsafe extern "C" fn(*mut Arglist) -> f64;
181
182pub type AddfuncFn = unsafe extern "C" fn(
184 name: *const c_char,
185 f: Rfunc,
186 ty: c_int,
187 nargs: c_int,
188 funcinfo: *mut c_void,
189 ae: *mut AmplExports,
190);
191
192pub type RandSeedSetter = unsafe extern "C" fn(*mut c_void, std::os::raw::c_ulong);
194
195pub type AddrandinitFn =
197 unsafe extern "C" fn(ae: *mut AmplExports, setter: RandSeedSetter, v: *mut c_void);
198
199pub type AtResetFn = unsafe extern "C" fn(ae: *mut AmplExports, f: *mut c_void, v: *mut c_void);
201
202#[repr(C)]
207pub struct AmplExports {
208 pub std_err: *mut c_void,
209 pub addfunc: Option<AddfuncFn>,
210 pub asl_date: c_long,
211 pub fprintf: *mut c_void,
212 pub printf: *mut c_void,
213 pub sprintf: *mut c_void,
214 pub vfprintf: *mut c_void,
215 pub vsprintf: *mut c_void,
216 pub strtod: *mut c_void,
217 pub crypto: *mut c_void,
218 pub asl: *mut c_char,
219 pub at_exit: *mut c_void,
220 pub at_reset: Option<AtResetFn>,
221 pub tempmem: *mut c_void,
222 pub add_table_handler: *mut c_void,
223 pub private_ae: *mut c_char,
224 pub qsortv: *mut c_void,
225
226 pub std_in: *mut c_void,
227 pub std_out: *mut c_void,
228 pub clearerr: *mut c_void,
229 pub fclose: *mut c_void,
230 pub fdopen: *mut c_void,
231 pub feof: *mut c_void,
232 pub ferror: *mut c_void,
233 pub fflush: *mut c_void,
234 pub fgetc: *mut c_void,
235 pub fgets: *mut c_void,
236 pub fileno: *mut c_void,
237 pub fopen: *mut c_void,
238 pub fputc: *mut c_void,
239 pub fputs: *mut c_void,
240 pub fread: *mut c_void,
241 pub freopen: *mut c_void,
242 pub fscanf: *mut c_void,
243 pub fseek: *mut c_void,
244 pub ftell: *mut c_void,
245 pub fwrite: *mut c_void,
246 pub pclose: *mut c_void,
247 pub perror: *mut c_void,
248 pub popen: *mut c_void,
249 pub puts: *mut c_void,
250 pub rewind: *mut c_void,
251 pub scanf: *mut c_void,
252 pub setbuf: *mut c_void,
253 pub setvbuf: *mut c_void,
254 pub sscanf: *mut c_void,
255 pub tempnam: *mut c_void,
256 pub tmpfile: *mut c_void,
257 pub tmpnam: *mut c_void,
258 pub ungetc: *mut c_void,
259 pub ai: *mut c_void,
260 pub getenv: *mut c_void,
261 pub breakfunc: *mut c_void,
262 pub breakarg: *mut c_char,
263
264 pub snprintf: *mut c_void,
266 pub vsnprintf: *mut c_void,
267
268 pub addrand: *mut c_void,
269 pub addrandinit: Option<AddrandinitFn>,
270}
271
272unsafe impl Send for AmplExports {}
278unsafe impl Sync for AmplExports {}
279
280#[derive(Debug, Clone)]
283pub struct RegisteredFunc {
284 pub name: String,
285 pub rfunc: Rfunc,
286 pub ty: i32,
288 pub nargs: i32,
291 pub funcinfo: *mut c_void,
293}
294
295unsafe impl Send for RegisteredFunc {}
299unsafe impl Sync for RegisteredFunc {}
300
301impl std::fmt::Debug for ExternalLibrary {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 f.debug_struct("ExternalLibrary")
304 .field("funcs", &self.funcs.keys().collect::<Vec<_>>())
305 .finish()
306 }
307}
308
309pub struct ExternalLibrary {
311 _lib: Arc<Library>,
314 _ae: Box<AmplExports>,
318 funcs: HashMap<String, RegisteredFunc>,
320}
321
322impl ExternalLibrary {
323 pub fn load(path: &Path) -> Result<Self, String> {
326 let _guard = ampl_lock().lock().unwrap_or_else(|e| e.into_inner());
329 let lib = unsafe { Library::new(path) }
333 .map_err(|e| format!("failed to open '{}': {}", path.display(), e))?;
334
335 type FuncaddFn = unsafe extern "C" fn(*mut AmplExports);
338 let funcadd: Symbol<FuncaddFn> = unsafe { lib.get(b"funcadd_ASL\0") }
339 .map_err(|e| format!("no funcadd_ASL in '{}': {}", path.display(), e))?;
340
341 let mut ae = Box::new(AmplExports {
345 std_err: ptr::null_mut(),
346 addfunc: Some(trampoline_addfunc),
347 asl_date: 20160307,
350 fprintf: ptr::null_mut(),
351 printf: ptr::null_mut(),
352 sprintf: ptr::null_mut(),
353 vfprintf: ptr::null_mut(),
354 vsprintf: ptr::null_mut(),
355 strtod: ptr::null_mut(),
356 crypto: ptr::null_mut(),
357 asl: ptr::null_mut(),
358 at_exit: ptr::null_mut(),
359 at_reset: Some(trampoline_atreset),
360 tempmem: ptr::null_mut(),
361 add_table_handler: ptr::null_mut(),
362 private_ae: ptr::null_mut(),
363 qsortv: ptr::null_mut(),
364 std_in: ptr::null_mut(),
365 std_out: ptr::null_mut(),
366 clearerr: ptr::null_mut(),
367 fclose: ptr::null_mut(),
368 fdopen: ptr::null_mut(),
369 feof: ptr::null_mut(),
370 ferror: ptr::null_mut(),
371 fflush: ptr::null_mut(),
372 fgetc: ptr::null_mut(),
373 fgets: ptr::null_mut(),
374 fileno: ptr::null_mut(),
375 fopen: ptr::null_mut(),
376 fputc: ptr::null_mut(),
377 fputs: ptr::null_mut(),
378 fread: ptr::null_mut(),
379 freopen: ptr::null_mut(),
380 fscanf: ptr::null_mut(),
381 fseek: ptr::null_mut(),
382 ftell: ptr::null_mut(),
383 fwrite: ptr::null_mut(),
384 pclose: ptr::null_mut(),
385 perror: ptr::null_mut(),
386 popen: ptr::null_mut(),
387 puts: ptr::null_mut(),
388 rewind: ptr::null_mut(),
389 scanf: ptr::null_mut(),
390 setbuf: ptr::null_mut(),
391 setvbuf: ptr::null_mut(),
392 sscanf: ptr::null_mut(),
393 tempnam: ptr::null_mut(),
394 tmpfile: ptr::null_mut(),
395 tmpnam: ptr::null_mut(),
396 ungetc: ptr::null_mut(),
397 ai: ptr::null_mut(),
398 getenv: ptr::null_mut(),
399 breakfunc: ptr::null_mut(),
400 breakarg: ptr::null_mut(),
401 snprintf: ptr::null_mut(),
402 vsnprintf: ptr::null_mut(),
403 addrand: ptr::null_mut(),
404 addrandinit: Some(trampoline_addrandinit),
405 });
406
407 REGISTRY_SINK.with(|sink| {
410 let mut guard = sink.borrow_mut();
411 assert!(
412 guard.is_none(),
413 "nested ExternalLibrary::load is not supported"
414 );
415 *guard = Some(HashMap::new());
416 });
417
418 unsafe { funcadd(ae.as_mut()) };
421
422 let funcs = REGISTRY_SINK
423 .with(|sink| sink.borrow_mut().take())
424 .unwrap_or_default();
425
426 Ok(ExternalLibrary {
427 _lib: Arc::new(lib),
428 _ae: ae,
429 funcs,
430 })
431 }
432
433 pub fn function_names(&self) -> impl Iterator<Item = &str> {
435 self.funcs.keys().map(|s| s.as_str())
436 }
437
438 pub fn get(&self, name: &str) -> Option<&RegisteredFunc> {
440 self.funcs.get(name)
441 }
442
443 pub fn eval(
455 &self,
456 name: &str,
457 args: &[ExternalArg<'_>],
458 want_derivs: bool,
459 want_hes: bool,
460 ) -> Result<EvalResult, String> {
461 let rf = self
462 .funcs
463 .get(name)
464 .ok_or_else(|| format!("no such external function '{name}'"))?;
465
466 let n = args.len() as i32;
468 if rf.nargs >= 0 {
469 if rf.nargs != n {
470 return Err(format!(
471 "external '{name}' expects {} args, got {}",
472 rf.nargs, n
473 ));
474 }
475 } else {
476 let min_args = -(rf.nargs + 1);
478 if n < min_args {
479 return Err(format!(
480 "external '{name}' expects at least {min_args} args, got {n}"
481 ));
482 }
483 }
484
485 let mut at_vec: Vec<c_int> = Vec::with_capacity(args.len());
487 let mut ra_vec: Vec<f64> = Vec::new();
488 let mut sa_owned: Vec<CString> = Vec::new();
489 for a in args {
490 match a {
491 ExternalArg::Real(x) => {
492 at_vec.push(ra_vec.len() as c_int);
493 ra_vec.push(*x);
494 }
495 ExternalArg::Str(s) => {
496 let cs = CString::new(*s)
497 .map_err(|_| format!("external '{name}' string arg contains NUL"))?;
498 at_vec.push(-(sa_owned.len() as c_int + 1));
499 sa_owned.push(cs);
500 }
501 }
502 }
503 let nr = ra_vec.len() as c_int;
504 let sa_ptrs: Vec<*const c_char> = sa_owned.iter().map(|s| s.as_ptr()).collect();
505
506 let has_strings = !sa_owned.is_empty();
509 if has_strings && (rf.ty & FUNCADD_STRING_ARGS) == 0 {
510 return Err(format!(
511 "external '{name}' is not declared FUNCADD_STRING_ARGS but was \
512 called with string arguments"
513 ));
514 }
515
516 let mut derivs_buf: Vec<f64> = if want_derivs {
518 vec![0.0; nr as usize]
519 } else {
520 Vec::new()
521 };
522 let hes_len = if want_hes {
523 (nr as usize) * ((nr as usize) + 1) / 2
524 } else {
525 0
526 };
527 let mut hes_buf: Vec<f64> = if want_hes {
528 vec![0.0; hes_len]
529 } else {
530 Vec::new()
531 };
532
533 let mut errmsg_buf: Vec<c_char> = vec![0; 1024];
540 let errmsg_orig_ptr = errmsg_buf.as_ptr();
541
542 let mut al = Arglist {
546 n,
547 nr,
548 at: if at_vec.is_empty() {
549 ptr::null_mut()
550 } else {
551 at_vec.as_mut_ptr()
552 },
553 ra: if ra_vec.is_empty() {
554 ptr::null_mut()
555 } else {
556 ra_vec.as_mut_ptr()
557 },
558 sa: if sa_ptrs.is_empty() {
559 ptr::null_mut()
560 } else {
561 sa_ptrs.as_ptr() as *mut *const c_char
562 },
563 derivs: if want_derivs {
564 derivs_buf.as_mut_ptr()
565 } else {
566 ptr::null_mut()
567 },
568 hes: if want_hes {
569 hes_buf.as_mut_ptr()
570 } else {
571 ptr::null_mut()
572 },
573 dig: ptr::null_mut(),
574 funcinfo: rf.funcinfo,
575 ae: self._ae_ptr(),
578 f: ptr::null_mut(),
579 tva: ptr::null_mut(),
580 errmsg: errmsg_buf.as_mut_ptr(),
581 tmi: ptr::null_mut(),
582 private: ptr::null_mut(),
583 nin: 0,
584 nout: 0,
585 nsin: 0,
586 nsout: 0,
587 };
588
589 let _guard = ampl_lock().lock().unwrap_or_else(|e| e.into_inner());
593 let value = unsafe { (rf.rfunc)(&mut al as *mut Arglist) };
594 drop(_guard);
595
596 if let Some(msg) =
603 unsafe { decode_external_errmsg(al.errmsg, errmsg_orig_ptr, errmsg_buf[0]) }
604 {
605 return Err(format!("external '{name}' reported: {msg}"));
606 }
607
608 Ok(EvalResult {
609 value,
610 derivs: if want_derivs { Some(derivs_buf) } else { None },
611 hessian: if want_hes { Some(hes_buf) } else { None },
612 })
613 }
614
615 fn _ae_ptr(&self) -> *mut AmplExports {
619 (&*self._ae as *const AmplExports) as *mut AmplExports
621 }
622}
623
624#[derive(Debug, Clone, Copy)]
626pub enum ExternalArg<'a> {
627 Real(f64),
628 Str(&'a str),
629}
630
631#[derive(Debug, Clone)]
633pub struct EvalResult {
634 pub value: f64,
636 pub derivs: Option<Vec<f64>>,
638 pub hessian: Option<Vec<f64>>,
641}
642
643unsafe fn decode_external_errmsg(
666 errmsg_field: *const c_char,
667 orig_buf_ptr: *const c_char,
668 buf_first: c_char,
669) -> Option<String> {
670 if !errmsg_field.is_null() && errmsg_field != orig_buf_ptr {
671 return Some(
674 unsafe { CStr::from_ptr(errmsg_field) }
675 .to_string_lossy()
676 .into_owned(),
677 );
678 }
679 if buf_first != 0 {
680 return Some(
683 unsafe { CStr::from_ptr(orig_buf_ptr) }
684 .to_string_lossy()
685 .into_owned(),
686 );
687 }
688 None
689}
690
691thread_local! {
701 static REGISTRY_SINK: std::cell::RefCell<Option<HashMap<String, RegisteredFunc>>> =
702 std::cell::RefCell::new(None);
703}
704
705unsafe extern "C" fn trampoline_addfunc(
707 name: *const c_char,
708 f: Rfunc,
709 ty: c_int,
710 nargs: c_int,
711 funcinfo: *mut c_void,
712 _ae: *mut AmplExports,
713) {
714 if name.is_null() {
715 return;
716 }
717 let cname = unsafe { CStr::from_ptr(name) };
719 let name_str = match cname.to_str() {
720 Ok(s) => s.to_owned(),
721 Err(_) => return, };
723 REGISTRY_SINK.with(|sink| {
724 if let Some(map) = sink.borrow_mut().as_mut() {
725 map.insert(
726 name_str.clone(),
727 RegisteredFunc {
728 name: name_str,
729 rfunc: f,
730 ty: ty as i32,
731 nargs: nargs as i32,
732 funcinfo,
733 },
734 );
735 }
736 });
737}
738
739unsafe extern "C" fn trampoline_atreset(_ae: *mut AmplExports, _f: *mut c_void, _v: *mut c_void) {
742 tracing::debug!("external library registered an AtReset callback; ignoring");
743}
744
745unsafe extern "C" fn trampoline_addrandinit(
748 _ae: *mut AmplExports,
749 setter: RandSeedSetter,
750 v: *mut c_void,
751) {
752 unsafe { setter(v, 1) };
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 fn idaes_dylib() -> Option<std::path::PathBuf> {
760 let home = std::env::var_os("HOME")?;
761 let p = std::path::PathBuf::from(home).join(".idaes/bin/general_helmholtz_external.dylib");
762 if p.exists() {
763 Some(p)
764 } else {
765 None
766 }
767 }
768
769 fn idaes_params_dir() -> Option<String> {
770 let home = std::env::var_os("HOME")?;
771 let p = std::path::PathBuf::from(home).join(
772 "Dropbox/uv/.venv/lib/python3.12/site-packages/idaes/\
773 models/properties/general_helmholtz/components/parameters/",
774 );
775 if p.exists() {
776 p.to_str().map(|s| s.to_owned())
777 } else {
778 None
779 }
780 }
781
782 #[test]
785 fn load_idaes_helmholtz_dylib_registers_known_functions() {
786 let Some(path) = idaes_dylib() else {
787 eprintln!("skipping: IDAES dylib not present");
788 return;
789 };
790
791 let lib = ExternalLibrary::load(&path).expect("load should succeed");
792 let names: Vec<String> = lib.function_names().map(|s| s.to_owned()).collect();
793
794 for required in &["vf_hp", "h_liq_hp", "h_vap_hp"] {
795 assert!(
796 names.iter().any(|n| n == required),
797 "expected {required} in registered names: {names:?}"
798 );
799 }
800 }
801
802 #[test]
806 fn eval_vf_hp_at_fixture_initial_point() {
807 let Some(path) = idaes_dylib() else {
808 eprintln!("skipping: IDAES dylib not present");
809 return;
810 };
811 let Some(params_dir) = idaes_params_dir() else {
812 eprintln!("skipping: IDAES parameters directory not present");
813 return;
814 };
815
816 let lib = ExternalLibrary::load(&path).expect("load");
817 let args = [
822 ExternalArg::Str("h2o"),
823 ExternalArg::Real(1878.71 * 0.055508472036052976),
824 ExternalArg::Real(101325.0 * 0.001),
825 ExternalArg::Str(¶ms_dir),
826 ];
827 let res = lib.eval("vf_hp", &args, false, false).expect("eval");
828 assert!(
829 res.value.is_finite(),
830 "vf_hp returned non-finite value {}",
831 res.value
832 );
833 }
834
835 #[test]
838 fn eval_vf_hp_with_derivatives() {
839 let Some(path) = idaes_dylib() else {
840 eprintln!("skipping: IDAES dylib not present");
841 return;
842 };
843 let Some(params_dir) = idaes_params_dir() else {
844 eprintln!("skipping: IDAES parameters directory not present");
845 return;
846 };
847
848 let lib = ExternalLibrary::load(&path).expect("load");
849 let args = [
850 ExternalArg::Str("h2o"),
851 ExternalArg::Real(1878.71 * 0.055508472036052976),
852 ExternalArg::Real(101325.0 * 0.001),
853 ExternalArg::Str(¶ms_dir),
854 ];
855 let res = lib.eval("vf_hp", &args, true, false).expect("eval");
856 let derivs = res.derivs.expect("derivs requested");
857 assert_eq!(derivs.len(), 2, "nr=2 reals -> 2 derivatives");
858 for (i, d) in derivs.iter().enumerate() {
859 assert!(d.is_finite(), "derivs[{i}] = {d} not finite");
860 }
861 }
862
863 #[test]
866 fn eval_vf_hp_with_hessian() {
867 let Some(path) = idaes_dylib() else {
868 eprintln!("skipping: IDAES dylib not present");
869 return;
870 };
871 let Some(params_dir) = idaes_params_dir() else {
872 eprintln!("skipping: IDAES parameters directory not present");
873 return;
874 };
875
876 let lib = ExternalLibrary::load(&path).expect("load");
877 let args = [
878 ExternalArg::Str("h2o"),
879 ExternalArg::Real(1878.71 * 0.055508472036052976),
880 ExternalArg::Real(101325.0 * 0.001),
881 ExternalArg::Str(¶ms_dir),
882 ];
883 let res = lib.eval("vf_hp", &args, true, true).expect("eval");
884 let hes = res.hessian.expect("hessian requested");
885 assert_eq!(hes.len(), 3, "nr=2 -> packed Hessian of length 3");
886 for (i, h) in hes.iter().enumerate() {
887 assert!(h.is_finite(), "hes[{i}] = {h} not finite");
888 }
889 }
890
891 unsafe extern "C" fn rfunc_reassigns_errmsg(al: *mut Arglist) -> f64 {
898 static MSG: &[u8] = b"T out of range\0";
899 unsafe {
901 (*al).errmsg = MSG.as_ptr() as *mut c_char;
902 }
903 f64::NAN
904 }
905
906 fn null_arglist(errmsg: *mut c_char) -> Arglist {
909 Arglist {
910 n: 1,
911 nr: 1,
912 at: ptr::null_mut(),
913 ra: ptr::null_mut(),
914 sa: ptr::null_mut(),
915 derivs: ptr::null_mut(),
916 hes: ptr::null_mut(),
917 dig: ptr::null_mut(),
918 funcinfo: ptr::null_mut(),
919 ae: ptr::null_mut(),
920 f: ptr::null_mut(),
921 tva: ptr::null_mut(),
922 errmsg,
923 tmi: ptr::null_mut(),
924 private: ptr::null_mut(),
925 nin: 0,
926 nout: 0,
927 nsin: 0,
928 nsout: 0,
929 }
930 }
931
932 #[test]
938 fn reassigned_errmsg_pointer_is_detected_end_to_end() {
939 let mut errmsg_buf: Vec<c_char> = vec![0; 1024];
940 let orig_ptr = errmsg_buf.as_ptr();
941 let mut al = null_arglist(errmsg_buf.as_mut_ptr());
942
943 let v = unsafe { rfunc_reassigns_errmsg(&mut al) };
945 assert!(v.is_nan(), "the failing eval returned NaN");
946
947 assert_eq!(
950 errmsg_buf[0], 0,
951 "a reassigning library must not touch the caller buffer"
952 );
953
954 let decoded = unsafe { decode_external_errmsg(al.errmsg, orig_ptr, errmsg_buf[0]) };
956 assert_eq!(
957 decoded.as_deref(),
958 Some("T out of range"),
959 "the reassigned errmsg pointer must be surfaced as an error"
960 );
961 }
962
963 #[test]
966 fn decode_external_errmsg_buffer_and_none_channels() {
967 let mut buf: Vec<c_char> = vec![0; 16];
969 for (i, b) in b"bad input".iter().enumerate() {
970 buf[i] = *b as c_char;
971 }
972 let orig = buf.as_ptr();
973 let decoded = unsafe { decode_external_errmsg(orig, orig, buf[0]) };
974 assert_eq!(decoded.as_deref(), Some("bad input"));
975
976 let zero: Vec<c_char> = vec![0; 16];
978 let z = zero.as_ptr();
979 assert_eq!(unsafe { decode_external_errmsg(z, z, zero[0]) }, None);
980
981 assert_eq!(unsafe { decode_external_errmsg(ptr::null(), z, 0) }, None);
983 }
984}