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