Skip to main content

llama_cpp_4/
shim.rs

1//! Plumbing shared by every wrapper over a C shim.
2//!
3//! `chat_shim` and `common_shim` agree on a status enum, a thread-local error
4//! buffer, and a size-then-fill protocol for variable-length output. This is the
5//! Rust half of that agreement: one error type and one set of helpers, so the
6//! modules built on them cannot drift.
7
8use std::ffi::{c_char, CStr, NulError};
9
10use llama_cpp_sys_4 as sys;
11
12use crate::token::LlamaToken;
13
14/// A failure inside one of llama.cpp's C++ layers, reported through a shim.
15///
16/// One type for every shim-backed module. The variants are the union of what
17/// the shims can report; a given call only produces the ones its documentation
18/// names.
19#[derive(Debug, thiserror::Error)]
20pub enum ShimError {
21    /// An argument contained an interior NUL byte, so it could not become a
22    /// `CString`. Caught in Rust — truncating at the NUL would silently change
23    /// the value.
24    #[error("argument contained an interior NUL byte")]
25    Nul(#[from] NulError),
26    /// A required pointer argument was null, or a value was out of range.
27    #[error("invalid argument passed to the shim")]
28    InvalidArg,
29    /// Input was not valid JSON.
30    #[error("input was not valid JSON: {0}")]
31    BadJson(String),
32    /// llama.cpp threw. The message is what the exception carried.
33    #[error("llama.cpp call failed: {0}")]
34    Failed(String),
35    /// A constructor returned null.
36    #[error("could not construct: {0}")]
37    Init(String),
38    /// Output was not valid UTF-8.
39    #[error("llama.cpp returned non-UTF-8 output")]
40    Utf8(#[from] std::string::FromUtf8Error),
41    /// A shim reported an offset outside the buffer it described, which would
42    /// mean the shim and this crate disagree about the layout.
43    #[error("shim reported an out-of-range offset")]
44    CorruptResult,
45}
46
47pub(crate) type Result<T> = std::result::Result<T, ShimError>;
48
49/// Detail for the most recent shim failure on this thread.
50///
51/// One buffer across all shims, so this is always about whatever just failed.
52pub(crate) fn last_error() -> String {
53    // SAFETY: the shim takes no arguments and returns a pointer into its own
54    // thread-local error buffer. The null case is handled below rather than
55    // assumed away.
56    let ptr = unsafe { sys::llama_shim_last_error() };
57    if ptr.is_null() {
58        return String::new();
59    }
60    // SAFETY: the pointer is non-null and points at a NUL-terminated string the
61    // library owns, which outlives the borrow taken here.
62    unsafe { CStr::from_ptr(ptr) }
63        .to_string_lossy()
64        .into_owned()
65}
66
67/// Turn a shim status into a `Result`.
68pub(crate) fn check_status(status: i32) -> Result<()> {
69    match status {
70        sys::LLAMA_SHIM_OK => Ok(()),
71        sys::LLAMA_SHIM_INVALID_ARG => Err(ShimError::InvalidArg),
72        sys::LLAMA_SHIM_BAD_JSON => Err(ShimError::BadJson(last_error())),
73        _ => Err(ShimError::Failed(last_error())),
74    }
75}
76
77/// Drive the size-then-fill protocol for a string output.
78///
79/// The first call sizes, the second fills. `BUFFER_TOO_SMALL` from the sizing
80/// call is the expected answer, not a failure.
81pub(crate) fn read_string<F>(mut call: F) -> Result<String>
82where
83    F: FnMut(*mut c_char, usize, *mut usize) -> i32,
84{
85    let mut needed: usize = 0;
86    let status = call(std::ptr::null_mut(), 0, &raw mut needed);
87    if status != sys::LLAMA_SHIM_BUFFER_TOO_SMALL {
88        check_status(status)?;
89    }
90    if needed == 0 {
91        return Ok(String::new());
92    }
93
94    let mut buf = vec![0u8; needed];
95    let status = call(
96        buf.as_mut_ptr().cast::<c_char>(),
97        buf.len(),
98        &raw mut needed,
99    );
100    check_status(status)?;
101
102    // Trim at the NUL the shim wrote rather than trusting `needed`, which the
103    // second call may have revised.
104    let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
105    buf.truncate(end);
106    String::from_utf8(buf).map_err(ShimError::from)
107}
108
109/// Drive the same protocol for a token-array output.
110pub(crate) fn read_tokens<F>(mut call: F) -> Result<Vec<LlamaToken>>
111where
112    F: FnMut(*mut i32, usize, *mut usize) -> i32,
113{
114    let mut needed: usize = 0;
115    let status = call(std::ptr::null_mut(), 0, &raw mut needed);
116    if status != sys::LLAMA_SHIM_BUFFER_TOO_SMALL {
117        check_status(status)?;
118    }
119    if needed == 0 {
120        return Ok(Vec::new());
121    }
122
123    let mut buf = vec![0i32; needed];
124    let status = call(buf.as_mut_ptr(), buf.len(), &raw mut needed);
125    check_status(status)?;
126    buf.truncate(needed);
127    Ok(buf.into_iter().map(LlamaToken).collect())
128}
129
130/// Drive the same protocol for an `i32` output that is not a token — a list of
131/// enum discriminants, say.
132pub(crate) fn read_i32s<F>(mut call: F) -> Result<Vec<i32>>
133where
134    F: FnMut(*mut i32, usize, *mut usize) -> i32,
135{
136    let mut needed: usize = 0;
137    let status = call(std::ptr::null_mut(), 0, &raw mut needed);
138    if status != sys::LLAMA_SHIM_BUFFER_TOO_SMALL {
139        check_status(status)?;
140    }
141    if needed == 0 {
142        return Ok(Vec::new());
143    }
144
145    let mut buf = vec![0i32; needed];
146    let status = call(buf.as_mut_ptr(), buf.len(), &raw mut needed);
147    check_status(status)?;
148    buf.truncate(needed);
149    Ok(buf)
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    /// The shims and this crate must agree on the status values; a mismatch
157    /// would map "bad JSON" onto "it threw" or vice versa. This is exactly the
158    /// bug that existed while `chat_shim` and `common_shim` had separate enums.
159    #[test]
160    fn status_codes_are_distinct_and_signed_as_documented() {
161        // Exact values, not just "negative": `check_status` dispatches on them,
162        // so a renumbering upstream would map one failure onto another.
163        assert_eq!(sys::LLAMA_SHIM_OK, 0);
164        assert_eq!(
165            sys::LLAMA_SHIM_BUFFER_TOO_SMALL,
166            1,
167            "a size query is not a failure, so it must be positive"
168        );
169        assert_eq!(sys::LLAMA_SHIM_INVALID_ARG, -1);
170        assert_eq!(sys::LLAMA_SHIM_BAD_JSON, -2);
171        assert_eq!(sys::LLAMA_SHIM_THROWN, -3);
172    }
173
174    #[test]
175    fn check_status_maps_each_code() {
176        assert!(check_status(sys::LLAMA_SHIM_OK).is_ok());
177        assert!(matches!(
178            check_status(sys::LLAMA_SHIM_INVALID_ARG),
179            Err(ShimError::InvalidArg)
180        ));
181        assert!(matches!(
182            check_status(sys::LLAMA_SHIM_BAD_JSON),
183            Err(ShimError::BadJson(_))
184        ));
185        assert!(matches!(
186            check_status(sys::LLAMA_SHIM_THROWN),
187            Err(ShimError::Failed(_))
188        ));
189    }
190
191    /// `last_error` reads a thread-local the shims share, so it must be safe to
192    /// call before anything has failed.
193    #[test]
194    fn last_error_is_empty_before_any_failure() {
195        // Not asserting emptiness — another test on this thread may have failed
196        // first. The contract is that it never dereferences null.
197        let _ = last_error();
198    }
199}