1use std::ffi::{c_char, c_void, CStr};
4use std::path::Path;
5
6use crate::Tokenizer;
7
8pub const ABI_VERSION: u32 = 1;
9
10pub mod symbol {
11 pub const ABI_VERSION: &[u8] = b"superfluid_tokenizer_abi_version\0";
12 pub const OPEN: &[u8] = b"superfluid_tokenizer_open\0";
13 pub const CLOSE: &[u8] = b"superfluid_tokenizer_close\0";
14 pub const ENCODE: &[u8] = b"superfluid_tokenizer_encode\0";
15 pub const TOKEN_BYTES: &[u8] = b"superfluid_tokenizer_token_bytes\0";
16 pub const VOCAB_SIZE: &[u8] = b"superfluid_tokenizer_vocab_size\0";
17 pub const SPECIAL_TOKENS: &[u8] = b"superfluid_tokenizer_special_tokens\0";
18 pub const BOS: &[u8] = b"superfluid_tokenizer_bos\0";
19 pub const EOS: &[u8] = b"superfluid_tokenizer_eos\0";
20 pub const CHAT_TEMPLATE: &[u8] = b"superfluid_tokenizer_chat_template\0";
21}
22
23pub type AbiVersionFn = unsafe extern "C" fn() -> u32;
24pub type OpenFn = unsafe extern "C" fn(path: *const c_char, err: *mut c_char, err_len: usize) -> *mut c_void;
25pub type CloseFn = unsafe extern "C" fn(tok: *mut c_void);
26pub type EncodeFn = unsafe extern "C" fn(tok: *const c_void, text: *const u8, len: usize, out: *mut u32, cap: usize) -> usize;
27pub type TokenBytesFn = unsafe extern "C" fn(tok: *const c_void, id: u32, out: *mut u8, cap: usize) -> usize;
28pub type VocabSizeFn = unsafe extern "C" fn(tok: *const c_void) -> u32;
29pub type SpecialTokensFn = unsafe extern "C" fn(tok: *const c_void, out: *mut u8, cap: usize) -> usize;
30pub type BosFn = unsafe extern "C" fn(tok: *const c_void) -> i64;
31pub type EosFn = unsafe extern "C" fn(tok: *const c_void) -> u32;
32pub type ChatTemplateFn = unsafe extern "C" fn(tok: *const c_void, name: *const c_char, out: *mut u8, cap: usize) -> usize;
33
34pub fn pack_special(tokens: &[(String, u32)]) -> Vec<u8> {
35 let mut out = Vec::new();
36 for (text, id) in tokens {
37 out.extend_from_slice(&id.to_le_bytes());
38 out.extend_from_slice(&(text.len() as u32).to_le_bytes());
39 out.extend_from_slice(text.as_bytes());
40 }
41 out
42}
43
44pub fn unpack_special(mut bytes: &[u8]) -> Vec<(String, u32)> {
45 let mut out = Vec::new();
46 while bytes.len() >= 8 {
47 let id = u32::from_le_bytes(bytes[0..4].try_into().expect("4 bytes"));
48 let len = u32::from_le_bytes(bytes[4..8].try_into().expect("4 bytes")) as usize;
49 let Some(text) = bytes.get(8..8 + len) else { break };
50 out.push((String::from_utf8_lossy(text).into_owned(), id));
51 bytes = &bytes[8 + len..];
52 }
53 out
54}
55
56#[doc(hidden)]
57pub mod export {
58 use super::*;
59 use std::panic::{catch_unwind, AssertUnwindSafe};
60
61 type Boxed = Box<dyn Tokenizer>;
62
63 fn fill(bytes: &[u8], out: *mut u8, cap: usize) -> usize {
64 let n = bytes.len().min(cap);
65 if n > 0 && !out.is_null() {
66 unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, n) };
68 }
69 bytes.len()
70 }
71
72 fn write_err(err: *mut c_char, err_len: usize, msg: &str) {
73 if err.is_null() || err_len == 0 {
74 return;
75 }
76 let n = msg.len().min(err_len - 1);
77 unsafe {
79 std::ptr::copy_nonoverlapping(msg.as_ptr(), err.cast::<u8>(), n);
80 *err.add(n) = 0;
81 }
82 }
83
84 unsafe fn tok<'a>(tok: *const c_void) -> Option<&'a dyn Tokenizer> {
87 (!tok.is_null()).then(|| unsafe { &**(tok as *const Boxed) })
89 }
90
91 pub unsafe fn open<T, E>(
94 path: *const c_char,
95 err: *mut c_char,
96 err_len: usize,
97 load: impl Fn(&Path) -> Result<T, E>,
98 ) -> *mut c_void
99 where
100 T: Tokenizer + 'static,
101 E: std::fmt::Display,
102 {
103 if path.is_null() {
104 write_err(err, err_len, "no path");
105 return std::ptr::null_mut();
106 }
107 let path = unsafe { CStr::from_ptr(path) }.to_string_lossy().into_owned();
109 match catch_unwind(AssertUnwindSafe(|| load(Path::new(&path)))) {
110 Ok(Ok(t)) => Box::into_raw(Box::new(Box::new(t) as Boxed)) as *mut c_void,
111 Ok(Err(e)) => {
112 write_err(err, err_len, &e.to_string());
113 std::ptr::null_mut()
114 }
115 Err(_) => {
116 write_err(err, err_len, "the tokenizer panicked while loading");
117 std::ptr::null_mut()
118 }
119 }
120 }
121
122 pub unsafe fn close(t: *mut c_void) {
125 if !t.is_null() {
126 let boxed = unsafe { Box::from_raw(t as *mut Boxed) };
128 let _ = catch_unwind(AssertUnwindSafe(|| drop(boxed)));
129 }
130 }
131
132 pub unsafe fn encode(t: *const c_void, text: *const u8, len: usize, out: *mut u32, cap: usize) -> usize {
135 catch_unwind(AssertUnwindSafe(|| {
136 let bytes = if len == 0 {
137 &[][..]
138 } else {
139 unsafe { std::slice::from_raw_parts(text, len) }
141 };
142 let Some(tk) = (unsafe { tok(t) }) else { return 0 };
144 let ids = tk.encode(&String::from_utf8_lossy(bytes));
145 let n = ids.len().min(cap);
146 if n > 0 && !out.is_null() {
147 unsafe { std::ptr::copy_nonoverlapping(ids.as_ptr(), out, n) };
149 }
150 ids.len()
151 }))
152 .unwrap_or(0)
153 }
154
155 pub unsafe fn token_bytes(t: *const c_void, id: u32, out: *mut u8, cap: usize) -> usize {
158 catch_unwind(AssertUnwindSafe(|| unsafe { tok(t) }.map_or(0, |tk| fill(&tk.token_bytes(id), out, cap))))
160 .unwrap_or(0)
161 }
162
163 pub unsafe fn vocab_size(t: *const c_void) -> u32 {
166 catch_unwind(AssertUnwindSafe(|| unsafe { tok(t) }.map_or(0, |tk| tk.vocab_size()))).unwrap_or(0)
168 }
169
170 pub unsafe fn special_tokens(t: *const c_void, out: *mut u8, cap: usize) -> usize {
173 catch_unwind(AssertUnwindSafe(|| {
175 unsafe { tok(t) }.map_or(0, |tk| fill(&pack_special(&tk.special_tokens()), out, cap))
177 }))
178 .unwrap_or(0)
179 }
180
181 pub unsafe fn bos(t: *const c_void) -> i64 {
184 catch_unwind(AssertUnwindSafe(|| {
186 unsafe { tok(t) }.and_then(|tk| tk.bos_token()).map_or(-1, i64::from)
188 }))
189 .unwrap_or(-1)
190 }
191
192 pub unsafe fn eos(t: *const c_void) -> u32 {
195 catch_unwind(AssertUnwindSafe(|| unsafe { tok(t) }.map_or(0, |tk| tk.eos_token()))).unwrap_or(0)
197 }
198
199 pub unsafe fn chat_template(t: *const c_void, name: *const c_char, out: *mut u8, cap: usize) -> usize {
202 catch_unwind(AssertUnwindSafe(|| {
203 let Some(tk) = (unsafe { tok(t) }) else { return usize::MAX };
205 let template = if name.is_null() {
206 Some(tk.chat_template_jinja())
207 } else {
208 tk.chat_template_named(&unsafe { CStr::from_ptr(name) }.to_string_lossy())
210 };
211 match template {
212 Some(s) => fill(s.as_bytes(), out, cap),
213 None => usize::MAX,
214 }
215 }))
216 .unwrap_or(usize::MAX)
217 }
218}
219
220#[macro_export]
221macro_rules! export_tokenizer {
222 ($load:expr) => {
223 #[no_mangle]
224 pub extern "C" fn superfluid_tokenizer_abi_version() -> u32 {
225 $crate::tokenizer_abi::ABI_VERSION
226 }
227
228 #[no_mangle]
231 pub unsafe extern "C" fn superfluid_tokenizer_open(
232 path: *const ::std::ffi::c_char,
233 err: *mut ::std::ffi::c_char,
234 err_len: usize,
235 ) -> *mut ::std::ffi::c_void {
236 unsafe { $crate::tokenizer_abi::export::open(path, err, err_len, $load) }
238 }
239
240 #[no_mangle]
243 pub unsafe extern "C" fn superfluid_tokenizer_close(tok: *mut ::std::ffi::c_void) {
244 unsafe { $crate::tokenizer_abi::export::close(tok) }
246 }
247
248 #[no_mangle]
251 pub unsafe extern "C" fn superfluid_tokenizer_encode(
252 tok: *const ::std::ffi::c_void,
253 text: *const u8,
254 len: usize,
255 out: *mut u32,
256 cap: usize,
257 ) -> usize {
258 unsafe { $crate::tokenizer_abi::export::encode(tok, text, len, out, cap) }
260 }
261
262 #[no_mangle]
265 pub unsafe extern "C" fn superfluid_tokenizer_token_bytes(
266 tok: *const ::std::ffi::c_void,
267 id: u32,
268 out: *mut u8,
269 cap: usize,
270 ) -> usize {
271 unsafe { $crate::tokenizer_abi::export::token_bytes(tok, id, out, cap) }
273 }
274
275 #[no_mangle]
278 pub unsafe extern "C" fn superfluid_tokenizer_vocab_size(tok: *const ::std::ffi::c_void) -> u32 {
279 unsafe { $crate::tokenizer_abi::export::vocab_size(tok) }
281 }
282
283 #[no_mangle]
286 pub unsafe extern "C" fn superfluid_tokenizer_special_tokens(
287 tok: *const ::std::ffi::c_void,
288 out: *mut u8,
289 cap: usize,
290 ) -> usize {
291 unsafe { $crate::tokenizer_abi::export::special_tokens(tok, out, cap) }
293 }
294
295 #[no_mangle]
298 pub unsafe extern "C" fn superfluid_tokenizer_bos(tok: *const ::std::ffi::c_void) -> i64 {
299 unsafe { $crate::tokenizer_abi::export::bos(tok) }
301 }
302
303 #[no_mangle]
306 pub unsafe extern "C" fn superfluid_tokenizer_eos(tok: *const ::std::ffi::c_void) -> u32 {
307 unsafe { $crate::tokenizer_abi::export::eos(tok) }
309 }
310
311 #[no_mangle]
314 pub unsafe extern "C" fn superfluid_tokenizer_chat_template(
315 tok: *const ::std::ffi::c_void,
316 name: *const ::std::ffi::c_char,
317 out: *mut u8,
318 cap: usize,
319 ) -> usize {
320 unsafe { $crate::tokenizer_abi::export::chat_template(tok, name, out, cap) }
322 }
323 };
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 struct PanicsOnDrop;
331
332 impl Tokenizer for PanicsOnDrop {
333 fn encode(&self, _text: &str) -> Vec<u32> {
334 Vec::new()
335 }
336 fn token_bytes(&self, _token: u32) -> Vec<u8> {
337 Vec::new()
338 }
339 fn vocab_size(&self) -> u32 {
340 1
341 }
342 fn special_tokens(&self) -> Vec<(String, u32)> {
343 Vec::new()
344 }
345 fn bos_token(&self) -> Option<u32> {
346 None
347 }
348 fn eos_token(&self) -> u32 {
349 0
350 }
351 fn chat_template_jinja(&self) -> String {
352 String::new()
353 }
354 }
355
356 impl Drop for PanicsOnDrop {
357 fn drop(&mut self) {
358 panic!("a tokenizer that panics on drop");
359 }
360 }
361
362 #[test]
363 fn closing_a_tokenizer_whose_drop_panics_does_not_unwind_into_the_caller() {
364 unsafe {
367 let h = export::open(c"unused".as_ptr(), std::ptr::null_mut(), 0, |_| Ok::<_, String>(PanicsOnDrop));
368 assert!(!h.is_null());
369 assert_eq!(export::vocab_size(h), 1);
370 export::close(h);
371 }
372 }
373
374 #[test]
375 fn a_null_handle_answers_each_call_with_its_failure_value() {
376 let null = std::ptr::null();
377 unsafe {
379 assert_eq!(export::encode(null, b"hi".as_ptr(), 2, std::ptr::null_mut(), 0), 0);
380 assert_eq!(export::token_bytes(null, 1, std::ptr::null_mut(), 0), 0);
381 assert_eq!(export::vocab_size(null), 0);
382 assert_eq!(export::special_tokens(null, std::ptr::null_mut(), 0), 0);
383 assert_eq!(export::bos(null), -1);
384 assert_eq!(export::eos(null), 0);
385 assert_eq!(export::chat_template(null, std::ptr::null(), std::ptr::null_mut(), 0), usize::MAX);
386 export::close(std::ptr::null_mut());
387 }
388 }
389
390 #[test]
391 fn special_tokens_pack_and_unpack() {
392 let t = vec![("<|im_start|>".to_string(), 151644), ("".to_string(), 7), ("é".to_string(), 3)];
393 assert_eq!(unpack_special(&pack_special(&t)), t);
394 let packed = pack_special(&t);
395 assert_eq!(unpack_special(&packed[..packed.len() - 1]).len(), 2, "a truncated tail is dropped");
396 }
397}