Skip to main content

gemstone_gci/
lib.rs

1//! Low-level GemStone/S GCI dynamic loader and raw ABI bindings.
2//!
3//! This crate intentionally does not require GemStone headers at build time.
4//! It loads `libgcirpc` at runtime and keeps all unsafe C ABI calls in one
5//! small layer that higher-level Rust and PyO3 crates can share.
6
7use libloading::{Library, Symbol};
8use std::env;
9use std::error::Error as StdError;
10use std::ffi::{c_char, c_double, c_int, c_uint, c_void, CStr};
11use std::fmt;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16pub type RawOop = u64;
17pub type Result<T> = std::result::Result<T, GciError>;
18
19pub const OOP_ILLEGAL: RawOop = 0x01;
20pub const OOP_NIL: RawOop = 0x14;
21pub const OOP_FALSE: RawOop = 0x0C;
22pub const OOP_TRUE: RawOop = 0x10C;
23pub const OOP_ASCII_NUL: RawOop = 0x1C;
24pub const GCI_INVALID_SESSION: RawOop = 0;
25pub const GCI_ENCRYPT_BUF_SIZE: usize = 1024;
26pub const GCI_LOGIN_PW_ENCRYPTED: RawOop = 0x1;
27pub const GCI_LOGIN_IS_GCSTS: RawOop = 0x2;
28pub const GCI_ERR_STR_SIZE: usize = 1024;
29pub const GCI_MAX_ERR_ARGS: usize = 10;
30
31const TAG_SMALLINT: RawOop = 0x2;
32const TAG_SMALLDOUBLE: RawOop = 0x6;
33const TAG_SPECIAL: RawOop = 0x4;
34const SMALLINT_SHIFT: u32 = 3;
35const CHAR_TAG_BYTE: RawOop = 0x1C;
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
38pub struct Oop(pub RawOop);
39
40impl Oop {
41    pub const ILLEGAL: Self = Self(OOP_ILLEGAL);
42    pub const NIL: Self = Self(OOP_NIL);
43    pub const FALSE: Self = Self(OOP_FALSE);
44    pub const TRUE: Self = Self(OOP_TRUE);
45
46    pub fn from_smallint(value: i64) -> Self {
47        Self(i64_to_smallint(value))
48    }
49
50    pub fn from_bool(value: bool) -> Self {
51        if value {
52            Self::TRUE
53        } else {
54            Self::FALSE
55        }
56    }
57
58    pub fn from_char(value: char) -> Self {
59        Self(char_to_oop(value))
60    }
61
62    pub fn raw(self) -> RawOop {
63        self.0
64    }
65
66    pub fn is_illegal(self) -> bool {
67        self.0 == OOP_ILLEGAL
68    }
69
70    pub fn is_nil(self) -> bool {
71        self.0 == OOP_NIL
72    }
73
74    pub fn is_boolean(self) -> bool {
75        matches!(self.0, OOP_TRUE | OOP_FALSE)
76    }
77
78    pub fn as_bool(self) -> Option<bool> {
79        match self.0 {
80            OOP_TRUE => Some(true),
81            OOP_FALSE => Some(false),
82            _ => None,
83        }
84    }
85
86    pub fn is_smallint(self) -> bool {
87        is_smallint(self.0)
88    }
89
90    pub fn as_smallint(self) -> Option<i64> {
91        self.is_smallint().then(|| smallint_to_i64(self.0))
92    }
93
94    pub fn is_char(self) -> bool {
95        is_char(self.0)
96    }
97
98    pub fn as_char(self) -> Result<Option<char>> {
99        if self.is_char() {
100            char_from_oop(self.0).map(Some)
101        } else {
102            Ok(None)
103        }
104    }
105}
106
107impl From<RawOop> for Oop {
108    fn from(value: RawOop) -> Self {
109        Self(value)
110    }
111}
112
113impl From<Oop> for RawOop {
114    fn from(value: Oop) -> Self {
115        value.0
116    }
117}
118
119#[repr(C)]
120#[derive(Clone, Copy)]
121pub struct GciErrSType {
122    pub category: RawOop,
123    pub context: RawOop,
124    pub exception_obj: RawOop,
125    pub args: [RawOop; GCI_MAX_ERR_ARGS],
126    pub number: c_int,
127    pub arg_count: c_int,
128    pub fatal: u8,
129    pub message: [c_char; GCI_ERR_STR_SIZE + 1],
130    pub reason: [c_char; GCI_ERR_STR_SIZE + 1],
131}
132
133impl Default for GciErrSType {
134    fn default() -> Self {
135        // SAFETY: GciErrSType is a plain C layout buffer. The all-zero state is
136        // the same initialization pattern used by ctypes before GciErr fills it.
137        unsafe { std::mem::zeroed() }
138    }
139}
140
141impl GciErrSType {
142    pub fn message_text(&self) -> String {
143        c_char_array_to_string(&self.message)
144    }
145
146    pub fn reason_text(&self) -> String {
147        c_char_array_to_string(&self.reason)
148    }
149
150    pub fn full_message(&self) -> String {
151        let message = self.message_text();
152        let reason = self.reason_text();
153        if reason.is_empty() || reason == message {
154            message
155        } else {
156            format!("{message} [{reason}]")
157        }
158    }
159}
160
161#[derive(Debug)]
162pub enum GciError {
163    LibraryNotFound,
164    LibraryLoad {
165        path: PathBuf,
166        source: libloading::Error,
167    },
168    Symbol {
169        path: PathBuf,
170        symbol: String,
171        source: libloading::Error,
172    },
173    ReadDir {
174        path: PathBuf,
175        source: std::io::Error,
176    },
177    InvalidCharOop(RawOop),
178}
179
180impl fmt::Display for GciError {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        match self {
183            Self::LibraryNotFound => write!(
184                f,
185                "Cannot find libgcirpc. Pass a library path or set GS_LIB_PATH/GS_LIB/GEMSTONE."
186            ),
187            Self::LibraryLoad { path, source } => {
188                write!(f, "cannot load {}: {source}", path.display())
189            }
190            Self::Symbol {
191                path,
192                symbol,
193                source,
194            } => write!(f, "{symbol} not found in {}: {source}", path.display()),
195            Self::ReadDir { path, source } => {
196                write!(f, "cannot read {}: {source}", path.display())
197            }
198            Self::InvalidCharOop(oop) => write!(f, "invalid GemStone character OOP {oop}"),
199        }
200    }
201}
202
203impl StdError for GciError {
204    fn source(&self) -> Option<&(dyn StdError + 'static)> {
205        match self {
206            Self::LibraryLoad { source, .. } => Some(source),
207            Self::Symbol { source, .. } => Some(source),
208            Self::ReadDir { source, .. } => Some(source),
209            Self::LibraryNotFound | Self::InvalidCharOop(_) => None,
210        }
211    }
212}
213
214#[derive(Clone)]
215pub struct GciLibrary {
216    library: Arc<Library>,
217    path: PathBuf,
218}
219
220impl GciLibrary {
221    pub fn load(lib_path: Option<PathBuf>) -> Result<Self> {
222        let path = resolve_library_path(lib_path)?;
223        // SAFETY: Loading a dynamic library can execute platform loader hooks.
224        // Callers opt into this by selecting the GemStone GCI library path.
225        unsafe { Self::load_path(path) }
226    }
227
228    /// Load a specific dynamic library path.
229    ///
230    /// # Safety
231    ///
232    /// The caller must ensure `path` points to a trusted GemStone GCI library
233    /// compatible with this ABI wrapper.
234    pub unsafe fn load_path(path: PathBuf) -> Result<Self> {
235        let library = Library::new(&path).map_err(|source| GciError::LibraryLoad {
236            path: path.clone(),
237            source,
238        })?;
239        Ok(Self {
240            library: Arc::new(library),
241            path,
242        })
243    }
244
245    pub fn path(&self) -> &Path {
246        &self.path
247    }
248
249    /// Call `GciInit`.
250    ///
251    /// # Safety
252    ///
253    /// The loaded library must be a compatible GemStone GCI library.
254    pub unsafe fn gci_init(&self) -> Result<c_int> {
255        let init: Symbol<unsafe extern "C" fn() -> c_int> = self.symbol(b"GciInit")?;
256        Ok(init())
257    }
258
259    /// Call `GciSetNet`.
260    ///
261    /// # Safety
262    ///
263    /// All C strings and optional password buffer pointers must remain valid
264    /// for the duration of the call.
265    pub unsafe fn gci_set_net(
266        &self,
267        stone_nrs: &CStr,
268        host_username: &CStr,
269        encrypted_host_password: *const c_char,
270        gem_service: &CStr,
271    ) -> Result<()> {
272        let set_net: Symbol<
273            unsafe extern "C" fn(*const c_char, *const c_char, *const c_char, *const c_char),
274        > = self.symbol(b"GciSetNet")?;
275        set_net(
276            stone_nrs.as_ptr(),
277            host_username.as_ptr(),
278            encrypted_host_password,
279            gem_service.as_ptr(),
280        );
281        Ok(())
282    }
283
284    /// Call `GciEncrypt`.
285    ///
286    /// # Safety
287    ///
288    /// `buffer` must point to writable memory of at least `buffer_size` bytes.
289    pub unsafe fn gci_encrypt(
290        &self,
291        password: &CStr,
292        buffer: *mut c_char,
293        buffer_size: c_uint,
294    ) -> Result<*mut c_char> {
295        let encrypt: Symbol<
296            unsafe extern "C" fn(*const c_char, *mut c_char, c_uint) -> *mut c_char,
297        > = self.symbol(b"GciEncrypt")?;
298        Ok(encrypt(password.as_ptr(), buffer, buffer_size))
299    }
300
301    /// Call `GciLoginEx`.
302    ///
303    /// # Safety
304    ///
305    /// The credential strings must remain valid for the duration of the call,
306    /// and `GciSetNet` should already have configured the target stone.
307    pub unsafe fn gci_login_ex(
308        &self,
309        username: &CStr,
310        password: &CStr,
311        flags: c_uint,
312        halt_on_error: c_int,
313    ) -> Result<c_int> {
314        let login: Symbol<
315            unsafe extern "C" fn(*const c_char, *const c_char, c_uint, c_int) -> c_int,
316        > = self.symbol(b"GciLoginEx")?;
317        Ok(login(
318            username.as_ptr(),
319            password.as_ptr(),
320            flags,
321            halt_on_error,
322        ))
323    }
324
325    /// Call `GciLogout`.
326    ///
327    /// # Safety
328    ///
329    /// The current GCI session id must refer to the session to log out.
330    pub unsafe fn gci_logout(&self) -> Result<c_int> {
331        let logout: Symbol<unsafe extern "C" fn() -> c_int> = self.symbol(b"GciLogout")?;
332        Ok(logout())
333    }
334
335    /// Call `GciCommit` with a typed error buffer.
336    ///
337    /// # Safety
338    ///
339    /// The current GCI session id must refer to an active session.
340    pub unsafe fn gci_commit(&self, err: &mut GciErrSType) -> Result<c_int> {
341        self.gci_commit_ptr(err as *mut GciErrSType as *mut c_void)
342    }
343
344    /// Call `GciCommit` with a raw error buffer.
345    ///
346    /// # Safety
347    ///
348    /// `err` must be null or point to a writable `GciErrSType`-compatible
349    /// buffer expected by the loaded GCI library.
350    pub unsafe fn gci_commit_ptr(&self, err: *mut c_void) -> Result<c_int> {
351        let commit: Symbol<unsafe extern "C" fn(*mut c_void) -> c_int> =
352            self.symbol(b"GciCommit")?;
353        Ok(commit(err))
354    }
355
356    /// Call `GciAbort` with a typed error buffer.
357    ///
358    /// # Safety
359    ///
360    /// The current GCI session id must refer to an active session.
361    pub unsafe fn gci_abort(&self, err: &mut GciErrSType) -> Result<c_int> {
362        self.gci_abort_ptr(err as *mut GciErrSType as *mut c_void)
363    }
364
365    /// Call `GciAbort` with a raw error buffer.
366    ///
367    /// # Safety
368    ///
369    /// `err` must be null or point to a writable `GciErrSType`-compatible
370    /// buffer expected by the loaded GCI library.
371    pub unsafe fn gci_abort_ptr(&self, err: *mut c_void) -> Result<c_int> {
372        let abort: Symbol<unsafe extern "C" fn(*mut c_void) -> c_int> = self.symbol(b"GciAbort")?;
373        Ok(abort(err))
374    }
375
376    /// Call `GciErr` with a typed error buffer.
377    ///
378    /// # Safety
379    ///
380    /// The current GCI session id must be valid when querying session errors.
381    pub unsafe fn gci_err(&self, err: &mut GciErrSType) -> Result<c_int> {
382        self.gci_err_ptr(err as *mut GciErrSType as *mut c_void)
383    }
384
385    /// Call `GciErr` with a raw error buffer.
386    ///
387    /// # Safety
388    ///
389    /// `err` must point to a writable `GciErrSType`-compatible buffer.
390    pub unsafe fn gci_err_ptr(&self, err: *mut c_void) -> Result<c_int> {
391        let gci_err: Symbol<unsafe extern "C" fn(*mut c_void) -> c_int> = self.symbol(b"GciErr")?;
392        Ok(gci_err(err))
393    }
394
395    /// Call `GciExecuteStr`.
396    ///
397    /// # Safety
398    ///
399    /// `source` must be valid for the duration of the call and `receiver` must
400    /// be a valid OOP in the current session, or `OOP_NIL`.
401    pub unsafe fn gci_execute_str(&self, source: &CStr, receiver: RawOop) -> Result<RawOop> {
402        let execute: Symbol<unsafe extern "C" fn(*const c_char, RawOop) -> RawOop> =
403            self.symbol(b"GciExecuteStr")?;
404        Ok(execute(source.as_ptr(), receiver))
405    }
406
407    /// Call `GciNewString`.
408    ///
409    /// # Safety
410    ///
411    /// `value` must be valid for the duration of the call and a session must be
412    /// active.
413    pub unsafe fn gci_new_string(&self, value: &CStr) -> Result<RawOop> {
414        let new_string: Symbol<unsafe extern "C" fn(*const c_char) -> RawOop> =
415            self.symbol(b"GciNewString")?;
416        Ok(new_string(value.as_ptr()))
417    }
418
419    /// Call `GciNewSymbol`.
420    ///
421    /// # Safety
422    ///
423    /// `value` must be valid for the duration of the call and a session must be
424    /// active.
425    pub unsafe fn gci_new_symbol(&self, value: &CStr) -> Result<RawOop> {
426        let new_symbol: Symbol<unsafe extern "C" fn(*const c_char) -> RawOop> =
427            self.symbol(b"GciNewSymbol")?;
428        Ok(new_symbol(value.as_ptr()))
429    }
430
431    /// Call `GciFltToOop`.
432    ///
433    /// # Safety
434    ///
435    /// A compatible GCI session must be active.
436    pub unsafe fn gci_flt_to_oop(&self, value: c_double) -> Result<RawOop> {
437        let flt_to_oop: Symbol<unsafe extern "C" fn(c_double) -> RawOop> =
438            self.symbol(b"GciFltToOop")?;
439        Ok(flt_to_oop(value))
440    }
441
442    /// Call `GciOopToFlt_`.
443    ///
444    /// # Safety
445    ///
446    /// `value` must point to writable memory for one `c_double`; `oop` must be
447    /// meaningful in the current session.
448    pub unsafe fn gci_oop_to_flt(&self, oop: RawOop, value: *mut c_double) -> Result<c_int> {
449        let oop_to_flt: Symbol<unsafe extern "C" fn(RawOop, *mut c_double) -> c_int> =
450            self.symbol(b"GciOopToFlt_")?;
451        Ok(oop_to_flt(oop, value))
452    }
453
454    /// Call `GciFetchSize_`.
455    ///
456    /// # Safety
457    ///
458    /// `oop` must be meaningful in the current session.
459    pub unsafe fn gci_fetch_size(&self, oop: RawOop) -> Result<i64> {
460        let fetch_size: Symbol<unsafe extern "C" fn(RawOop) -> i64> =
461            self.symbol(b"GciFetchSize_")?;
462        Ok(fetch_size(oop))
463    }
464
465    /// Call `GciFetchBytes_`.
466    ///
467    /// # Safety
468    ///
469    /// `buffer` must point to writable memory for at least `count` bytes;
470    /// `oop` must be meaningful in the current session.
471    pub unsafe fn gci_fetch_bytes(
472        &self,
473        oop: RawOop,
474        start: i64,
475        buffer: *mut c_char,
476        count: i64,
477    ) -> Result<i64> {
478        let fetch_bytes: Symbol<unsafe extern "C" fn(RawOop, i64, *mut c_char, i64) -> i64> =
479            self.symbol(b"GciFetchBytes_")?;
480        Ok(fetch_bytes(oop, start, buffer, count))
481    }
482
483    /// Call `GciFetchClass`.
484    ///
485    /// # Safety
486    ///
487    /// `oop` must be meaningful in the current session.
488    pub unsafe fn gci_fetch_class(&self, oop: RawOop) -> Result<RawOop> {
489        let fetch_class: Symbol<unsafe extern "C" fn(RawOop) -> RawOop> =
490            self.symbol(b"GciFetchClass")?;
491        Ok(fetch_class(oop))
492    }
493
494    /// Call `GciPerform`.
495    ///
496    /// # Safety
497    ///
498    /// `receiver` and each argument OOP must be meaningful in the current
499    /// session; `args` must point to at least `argc` OOPs unless `argc` is 0.
500    pub unsafe fn gci_perform(
501        &self,
502        receiver: RawOop,
503        selector: &CStr,
504        args: *const RawOop,
505        argc: c_int,
506    ) -> Result<RawOop> {
507        let perform: Symbol<
508            unsafe extern "C" fn(RawOop, *const c_char, *const RawOop, c_int) -> RawOop,
509        > = self.symbol(b"GciPerform")?;
510        Ok(perform(receiver, selector.as_ptr(), args, argc))
511    }
512
513    /// Call `GciNewOop`.
514    ///
515    /// # Safety
516    ///
517    /// `class_oop` must be a valid GemStone class OOP in the current session.
518    pub unsafe fn gci_new_oop(&self, class_oop: RawOop) -> Result<RawOop> {
519        let new_oop: Symbol<unsafe extern "C" fn(RawOop) -> RawOop> = self.symbol(b"GciNewOop")?;
520        Ok(new_oop(class_oop))
521    }
522
523    /// Call `GciResolveSymbol`.
524    ///
525    /// # Safety
526    ///
527    /// `name` must be valid for the duration of the call; `symbol_list` must be
528    /// a valid symbol list OOP or `OOP_NIL`.
529    pub unsafe fn gci_resolve_symbol(&self, name: &CStr, symbol_list: RawOop) -> Result<RawOop> {
530        let resolve: Symbol<unsafe extern "C" fn(*const c_char, RawOop) -> RawOop> =
531            self.symbol(b"GciResolveSymbol")?;
532        Ok(resolve(name.as_ptr(), symbol_list))
533    }
534
535    /// Call `GciSymDictAtPut`.
536    ///
537    /// # Safety
538    ///
539    /// `dict` must be a valid symbol dictionary OOP and `value` must be valid
540    /// in the current session.
541    pub unsafe fn gci_sym_dict_at_put(
542        &self,
543        dict: RawOop,
544        key: &CStr,
545        value: RawOop,
546    ) -> Result<()> {
547        let at_put: Symbol<unsafe extern "C" fn(RawOop, *const c_char, RawOop)> =
548            self.symbol(b"GciSymDictAtPut")?;
549        at_put(dict, key.as_ptr(), value);
550        Ok(())
551    }
552
553    /// Call `GciSymDictAtObjPut`.
554    ///
555    /// # Safety
556    ///
557    /// `dict`, `key`, and `value` must be valid OOPs in the current session.
558    pub unsafe fn gci_sym_dict_at_obj_put(
559        &self,
560        dict: RawOop,
561        key: RawOop,
562        value: RawOop,
563    ) -> Result<()> {
564        let at_put: Symbol<unsafe extern "C" fn(RawOop, RawOop, RawOop)> =
565            self.symbol(b"GciSymDictAtObjPut")?;
566        at_put(dict, key, value);
567        Ok(())
568    }
569
570    /// Call `GciStrKeyValueDictAtPut`.
571    ///
572    /// # Safety
573    ///
574    /// `dict` and `value` must be valid OOPs in the current session; `key`
575    /// must remain valid for the duration of the call.
576    pub unsafe fn gci_str_key_value_dict_at_put(
577        &self,
578        dict: RawOop,
579        key: &CStr,
580        value: RawOop,
581    ) -> Result<()> {
582        let at_put: Symbol<unsafe extern "C" fn(RawOop, *const c_char, RawOop)> =
583            self.symbol(b"GciStrKeyValueDictAtPut")?;
584        at_put(dict, key.as_ptr(), value);
585        Ok(())
586    }
587
588    /// Call `GciStrKeyValueDictAt`.
589    ///
590    /// # Safety
591    ///
592    /// `dict` must be valid in the current session; `key` must remain valid and
593    /// `value` must point to writable memory for one OOP.
594    pub unsafe fn gci_str_key_value_dict_at(
595        &self,
596        dict: RawOop,
597        key: &CStr,
598        value: *mut RawOop,
599    ) -> Result<()> {
600        let at: Symbol<unsafe extern "C" fn(RawOop, *const c_char, *mut RawOop)> =
601            self.symbol(b"GciStrKeyValueDictAt")?;
602        at(dict, key.as_ptr(), value);
603        Ok(())
604    }
605
606    /// Call `GciSymDictAt`.
607    ///
608    /// # Safety
609    ///
610    /// `dict` must be valid in the current session; `key` must remain valid and
611    /// `value`/`assoc` must point to writable memory for one OOP each.
612    pub unsafe fn gci_sym_dict_at(
613        &self,
614        dict: RawOop,
615        key: &CStr,
616        value: *mut RawOop,
617        assoc: *mut RawOop,
618    ) -> Result<()> {
619        let at: Symbol<unsafe extern "C" fn(RawOop, *const c_char, *mut RawOop, *mut RawOop)> =
620            self.symbol(b"GciSymDictAt")?;
621        at(dict, key.as_ptr(), value, assoc);
622        Ok(())
623    }
624
625    /// Call `GciGetSessionId`.
626    ///
627    /// # Safety
628    ///
629    /// The loaded library must be a compatible GemStone GCI library.
630    pub unsafe fn gci_get_session_id(&self) -> Result<c_int> {
631        let get_session_id: Symbol<unsafe extern "C" fn() -> c_int> =
632            self.symbol(b"GciGetSessionId")?;
633        Ok(get_session_id())
634    }
635
636    /// Call `GciSetSessionId`.
637    ///
638    /// # Safety
639    ///
640    /// `session_id` must be an id returned by GCI for this process, or the
641    /// caller must otherwise know it is valid.
642    pub unsafe fn gci_set_session_id(&self, session_id: c_int) -> Result<()> {
643        let set_session_id: Symbol<unsafe extern "C" fn(c_int)> =
644            self.symbol(b"GciSetSessionId")?;
645        set_session_id(session_id);
646        Ok(())
647    }
648
649    /// Call `GciNeedsCommit`.
650    ///
651    /// # Safety
652    ///
653    /// The current GCI session id must refer to an active session.
654    pub unsafe fn gci_needs_commit(&self) -> Result<c_int> {
655        let needs_commit: Symbol<unsafe extern "C" fn() -> c_int> =
656            self.symbol(b"GciNeedsCommit")?;
657        Ok(needs_commit())
658    }
659
660    /// Call `GciInTransaction`.
661    ///
662    /// # Safety
663    ///
664    /// The current GCI session id must refer to an active session.
665    pub unsafe fn gci_in_transaction(&self) -> Result<c_int> {
666        let in_transaction: Symbol<unsafe extern "C" fn() -> c_int> =
667            self.symbol(b"GciInTransaction")?;
668        Ok(in_transaction())
669    }
670
671    /// Call an optional export-set function when it exists in the loaded GCI library.
672    ///
673    /// # Safety
674    ///
675    /// `name` must be the exact symbol name of a GCI function with signature
676    /// `extern "C" fn(RawOop)`, and `oop` must be meaningful in the current
677    /// session.
678    pub unsafe fn call_optional_oop_export(&self, name: &[u8], oop: RawOop) -> Result<bool> {
679        let symbol = self.library.get::<unsafe extern "C" fn(RawOop)>(name);
680        if let Ok(function) = symbol {
681            function(oop);
682            return Ok(true);
683        }
684        Ok(false)
685    }
686
687    fn symbol<T>(&self, name: &[u8]) -> Result<Symbol<'_, T>> {
688        unsafe { self.library.get(name) }.map_err(|source| GciError::Symbol {
689            path: self.path.clone(),
690            symbol: String::from_utf8_lossy(name).to_string(),
691            source,
692        })
693    }
694}
695
696pub fn is_smallint(oop: RawOop) -> bool {
697    (oop & 0x7) == TAG_SMALLINT
698}
699
700pub fn is_smalldouble(oop: RawOop) -> bool {
701    (oop & 0x7) == TAG_SMALLDOUBLE
702}
703
704pub fn smallint_to_i64(oop: RawOop) -> i64 {
705    (oop as i64) >> SMALLINT_SHIFT
706}
707
708pub fn i64_to_smallint(value: i64) -> RawOop {
709    ((value << SMALLINT_SHIFT) as RawOop) | TAG_SMALLINT
710}
711
712pub fn is_char(oop: RawOop) -> bool {
713    (oop & 0xFF) == CHAR_TAG_BYTE && (oop & 0x6) == TAG_SPECIAL
714}
715
716pub fn char_from_oop(oop: RawOop) -> Result<char> {
717    let codepoint = ((oop >> 8) & 0x1F_FFFF) as u32;
718    char::from_u32(codepoint).ok_or(GciError::InvalidCharOop(oop))
719}
720
721pub fn char_to_oop(value: char) -> RawOop {
722    ((value as u32 as RawOop) << 8) | CHAR_TAG_BYTE
723}
724
725pub fn resolve_library_path(lib_path: Option<PathBuf>) -> Result<PathBuf> {
726    if let Some(path) = lib_path {
727        return Ok(path);
728    }
729    if let Ok(path) = env::var("GS_LIB_PATH") {
730        if !path.is_empty() {
731            return Ok(PathBuf::from(path));
732        }
733    }
734    if let Ok(dir) = env::var("GS_LIB") {
735        if !dir.is_empty() {
736            if let Some(path) = find_gcirpc_in_dir(Path::new(&dir))? {
737                return Ok(path);
738            }
739        }
740    }
741    if let Ok(gemstone) = env::var("GEMSTONE") {
742        if !gemstone.is_empty() {
743            let lib_dir = Path::new(&gemstone).join("lib");
744            if let Some(path) = find_gcirpc_in_dir(&lib_dir)? {
745                return Ok(path);
746            }
747        }
748    }
749    Err(GciError::LibraryNotFound)
750}
751
752pub fn find_gcirpc_in_dir(dir: &Path) -> Result<Option<PathBuf>> {
753    if !dir.is_dir() {
754        return Ok(None);
755    }
756    let mut candidates = Vec::new();
757    for entry in fs::read_dir(dir).map_err(|source| GciError::ReadDir {
758        path: dir.to_path_buf(),
759        source,
760    })? {
761        let entry = entry.map_err(|source| GciError::ReadDir {
762            path: dir.to_path_buf(),
763            source,
764        })?;
765        let path = entry.path();
766        let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
767            continue;
768        };
769        if name.starts_with("libgcirpc")
770            && (name.ends_with(".dylib") || name.ends_with(".so") || name.ends_with(".dll"))
771        {
772            candidates.push(path);
773        }
774    }
775    candidates.sort();
776    Ok(candidates.pop())
777}
778
779fn c_char_array_to_string(value: &[c_char]) -> String {
780    let bytes: Vec<u8> = value
781        .iter()
782        .take_while(|byte| **byte != 0)
783        .map(|byte| *byte as u8)
784        .collect();
785    String::from_utf8_lossy(&bytes).into_owned()
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    #[test]
793    fn smallint_helpers_round_trip_signed_values() {
794        for value in [-42, -1, 0, 1, 42] {
795            assert_eq!(smallint_to_i64(i64_to_smallint(value)), value);
796        }
797    }
798
799    #[test]
800    fn oop_helpers_identify_special_values() {
801        assert!(Oop::NIL.is_nil());
802        assert!(Oop::TRUE.is_boolean());
803        assert!(Oop::FALSE.is_boolean());
804        assert_eq!(Oop::TRUE.as_bool(), Some(true));
805        assert_eq!(Oop::FALSE.as_bool(), Some(false));
806        assert!(Oop(i64_to_smallint(7)).is_smallint());
807        assert_eq!(Oop(i64_to_smallint(7)).as_smallint(), Some(7));
808    }
809
810    #[test]
811    fn char_helpers_round_trip_unicode_values() {
812        for value in ['A', '\0', 'λ'] {
813            let oop = Oop::from_char(value);
814            assert!(oop.is_char());
815            assert_eq!(oop.as_char().unwrap(), Some(value));
816            assert_eq!(char_from_oop(char_to_oop(value)).unwrap(), value);
817        }
818    }
819}