Skip to main content

inchi_sys/
lib.rs

1//! Low-level, `unsafe` FFI bindings to the vendored IUPAC InChI 1.07 reference
2//! C library.
3//!
4//! This crate is the unsafe foundation underneath the safe [`inchi`] crate. It
5//! exposes the raw C structs and `extern "C"` entry points (`GetINCHI`,
6//! `GetINCHIKeyFromINCHI`, `MakeINCHIFromMolfileText`, `GetStructFromINCHI`,
7//! the matching `Free*` deallocators, ...) exactly as declared in the upstream
8//! `inchi_api.h`. The native C source is vendored and statically linked, so no
9//! system InChI installation is required.
10//!
11//! Almost all users should depend on the high-level [`inchi`] crate instead.
12//! Everything here is `unsafe` and mirrors C ownership rules: any `out`
13//! parameter populated by a `Get*`/`Make*` call **must** be released with the
14//! corresponding `Free*` call to avoid leaking memory allocated by the C side.
15//!
16//! [`inchi`]: https://docs.rs/inchi
17//!
18//! # Bindings
19//!
20//! By default the version-controlled, pre-generated bindings (produced by
21//! `bindgen`) are used, so no `libclang` is needed to build. Enable the
22//! `regenerate-bindings` feature to regenerate them from the vendored headers
23//! at build time.
24//!
25//! # Safety & threading
26//!
27//! The InChI C library keeps some `static` internal state and is **not**
28//! guaranteed to be thread-safe across concurrent calls. Callers of this crate
29//! are responsible for synchronization; the [`inchi`] crate provides it.
30#![allow(non_upper_case_globals)]
31#![allow(non_camel_case_types)]
32#![allow(non_snake_case)]
33#![allow(clippy::all)]
34
35#[cfg(feature = "regenerate-bindings")]
36mod bindings {
37    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
38}
39
40#[cfg(not(feature = "regenerate-bindings"))]
41#[path = "bindings.rs"]
42mod bindings;
43
44pub use bindings::*;
45
46#[cfg(test)]
47mod smoke {
48    //! Proves the native library links and produces a correct InChI for a
49    //! trivial structure built directly into an `inchi_Input`.
50    use super::*;
51    use std::ffi::CStr;
52    use std::os::raw::c_char;
53
54    /// A V2000 molfile for methane (single carbon; InChI adds implicit H).
55    const METHANE_MOLFILE: &str = "\n  inchi-sys\n\n  1  0  0  0  0  0  0  0  0  0999 V2000\n    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0\nM  END\n";
56
57    #[test]
58    fn methane_via_molfile_text() {
59        // Build NUL-terminated inputs for the C API.
60        let moltext = std::ffi::CString::new(METHANE_MOLFILE).expect("no interior NUL");
61        let options = std::ffi::CString::new("").expect("no interior NUL");
62
63        let mut out: inchi_Output = unsafe { std::mem::zeroed() };
64        let rc = unsafe {
65            MakeINCHIFromMolfileText(moltext.as_ptr(), options.as_ptr() as *mut c_char, &mut out)
66        };
67
68        // 0 == okay, 1 == warning; both yield a valid InChI string.
69        assert!(
70            rc == inchi_Ret_OKAY as i32 || rc == inchi_Ret_WARNING as i32,
71            "MakeINCHIFromMolfileText returned {rc}"
72        );
73        assert!(!out.szInChI.is_null(), "no InChI produced");
74
75        let inchi = unsafe { CStr::from_ptr(out.szInChI) }
76            .to_str()
77            .expect("InChI is valid UTF-8")
78            .to_owned();
79
80        // Release everything the C side allocated before asserting.
81        unsafe { FreeINCHI(&mut out) };
82
83        assert_eq!(inchi, "InChI=1S/CH4/h1H4");
84    }
85
86    /// Proves the manually maintained IXA declarations link against the
87    /// vendored library and produce the same InChI through the extensible API.
88    #[test]
89    fn methane_via_ixa() {
90        let molfile = std::ffi::CString::new(METHANE_MOLFILE).expect("no interior NUL");
91
92        unsafe {
93            let status = IXA_STATUS_Create();
94            assert!(!status.is_null(), "IXA_STATUS_Create returned null");
95            let mol = IXA_MOL_Create(status);
96            assert!(!mol.is_null(), "IXA_MOL_Create returned null");
97
98            IXA_MOL_ReadMolfile(status, mol, molfile.as_ptr());
99            assert_eq!(
100                IXA_STATUS_HasError(status),
101                IXA_FALSE,
102                "ReadMolfile errored"
103            );
104            assert_eq!(
105                IXA_MOL_GetNumAtoms(status, mol),
106                1,
107                "expected one heavy atom"
108            );
109
110            let builder = IXA_INCHIBUILDER_Create(status);
111            assert!(!builder.is_null(), "IXA_INCHIBUILDER_Create returned null");
112            IXA_INCHIBUILDER_SetMolecule(status, builder, mol);
113            let inchi_ptr = IXA_INCHIBUILDER_GetInChI(status, builder);
114            assert!(!inchi_ptr.is_null(), "GetInChI returned null");
115            let inchi = CStr::from_ptr(inchi_ptr)
116                .to_str()
117                .expect("InChI is valid UTF-8")
118                .to_owned();
119
120            IXA_INCHIBUILDER_Destroy(status, builder);
121            IXA_MOL_Destroy(status, mol);
122            IXA_STATUS_Destroy(status);
123
124            assert_eq!(inchi, "InChI=1S/CH4/h1H4");
125        }
126    }
127}