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    let ptr = unsafe { sys::llama_shim_last_error() };
54    if ptr.is_null() {
55        return String::new();
56    }
57    unsafe { CStr::from_ptr(ptr) }
58        .to_string_lossy()
59        .into_owned()
60}
61
62/// Turn a shim status into a `Result`.
63pub(crate) fn check_status(status: i32) -> Result<()> {
64    match status {
65        sys::LLAMA_SHIM_OK => Ok(()),
66        sys::LLAMA_SHIM_INVALID_ARG => Err(ShimError::InvalidArg),
67        sys::LLAMA_SHIM_BAD_JSON => Err(ShimError::BadJson(last_error())),
68        _ => Err(ShimError::Failed(last_error())),
69    }
70}
71
72/// Drive the size-then-fill protocol for a string output.
73///
74/// The first call sizes, the second fills. `BUFFER_TOO_SMALL` from the sizing
75/// call is the expected answer, not a failure.
76pub(crate) fn read_string<F>(mut call: F) -> Result<String>
77where
78    F: FnMut(*mut c_char, usize, *mut usize) -> i32,
79{
80    let mut needed: usize = 0;
81    let status = call(std::ptr::null_mut(), 0, &raw mut needed);
82    if status != sys::LLAMA_SHIM_BUFFER_TOO_SMALL {
83        check_status(status)?;
84    }
85    if needed == 0 {
86        return Ok(String::new());
87    }
88
89    let mut buf = vec![0u8; needed];
90    let status = call(buf.as_mut_ptr().cast::<c_char>(), buf.len(), &raw mut needed);
91    check_status(status)?;
92
93    // Trim at the NUL the shim wrote rather than trusting `needed`, which the
94    // second call may have revised.
95    let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
96    buf.truncate(end);
97    String::from_utf8(buf).map_err(ShimError::from)
98}
99
100/// Drive the same protocol for a token-array output.
101pub(crate) fn read_tokens<F>(mut call: F) -> Result<Vec<LlamaToken>>
102where
103    F: FnMut(*mut i32, usize, *mut usize) -> i32,
104{
105    let mut needed: usize = 0;
106    let status = call(std::ptr::null_mut(), 0, &raw mut needed);
107    if status != sys::LLAMA_SHIM_BUFFER_TOO_SMALL {
108        check_status(status)?;
109    }
110    if needed == 0 {
111        return Ok(Vec::new());
112    }
113
114    let mut buf = vec![0i32; needed];
115    let status = call(buf.as_mut_ptr(), buf.len(), &raw mut needed);
116    check_status(status)?;
117    buf.truncate(needed);
118    Ok(buf.into_iter().map(LlamaToken).collect())
119}
120
121/// Drive the same protocol for an `i32` output that is not a token — a list of
122/// enum discriminants, say.
123pub(crate) fn read_i32s<F>(mut call: F) -> Result<Vec<i32>>
124where
125    F: FnMut(*mut i32, usize, *mut usize) -> i32,
126{
127    let mut needed: usize = 0;
128    let status = call(std::ptr::null_mut(), 0, &raw mut needed);
129    if status != sys::LLAMA_SHIM_BUFFER_TOO_SMALL {
130        check_status(status)?;
131    }
132    if needed == 0 {
133        return Ok(Vec::new());
134    }
135
136    let mut buf = vec![0i32; needed];
137    let status = call(buf.as_mut_ptr(), buf.len(), &raw mut needed);
138    check_status(status)?;
139    buf.truncate(needed);
140    Ok(buf)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    /// The shims and this crate must agree on the status values; a mismatch
148    /// would map "bad JSON" onto "it threw" or vice versa. This is exactly the
149    /// bug that existed while `chat_shim` and `common_shim` had separate enums.
150    #[test]
151    fn status_codes_are_distinct_and_signed_as_documented() {
152        // Exact values, not just "negative": `check_status` dispatches on them,
153        // so a renumbering upstream would map one failure onto another.
154        assert_eq!(sys::LLAMA_SHIM_OK, 0);
155        assert_eq!(
156            sys::LLAMA_SHIM_BUFFER_TOO_SMALL, 1,
157            "a size query is not a failure, so it must be positive"
158        );
159        assert_eq!(sys::LLAMA_SHIM_INVALID_ARG, -1);
160        assert_eq!(sys::LLAMA_SHIM_BAD_JSON, -2);
161        assert_eq!(sys::LLAMA_SHIM_THROWN, -3);
162    }
163
164    #[test]
165    fn check_status_maps_each_code() {
166        assert!(check_status(sys::LLAMA_SHIM_OK).is_ok());
167        assert!(matches!(
168            check_status(sys::LLAMA_SHIM_INVALID_ARG),
169            Err(ShimError::InvalidArg)
170        ));
171        assert!(matches!(
172            check_status(sys::LLAMA_SHIM_BAD_JSON),
173            Err(ShimError::BadJson(_))
174        ));
175        assert!(matches!(
176            check_status(sys::LLAMA_SHIM_THROWN),
177            Err(ShimError::Failed(_))
178        ));
179    }
180
181    /// `last_error` reads a thread-local the shims share, so it must be safe to
182    /// call before anything has failed.
183    #[test]
184    fn last_error_is_empty_before_any_failure() {
185        // Not asserting emptiness — another test on this thread may have failed
186        // first. The contract is that it never dereferences null.
187        let _ = last_error();
188    }
189}