Skip to main content

colorfulcode_ffi/
lib.rs

1//! C ABI for colorfulcode, for native apps written in Swift, Kotlin, C++, C#
2//! and friends. The matching header is `include/colorfulcode.h`.
3//!
4//! Highlighting returns a flat array of `cc_token`s that covers the input,
5//! and `cc_theme_style` maps each token kind to a color, so drawing comes
6//! down to one attribute per token.
7//!
8//! Conventions:
9//! - Languages and themes are referred to by index. Indices are only stable
10//!   within one build of the library, so persist ids or names instead.
11//! - Input strings must be UTF-8. Functions taking source code return NULL
12//!   for invalid UTF-8 rather than guessing.
13//! - Null pointers are accepted everywhere and treated as missing input.
14//! - Panics never cross the boundary: they are caught, or abort in release
15//!   builds where `panic = "abort"`.
16
17use colorfulcode::{ansi, highlight, html, Kind, Language, Theme};
18use std::ffi::{c_char, CStr, CString};
19use std::panic::{catch_unwind, AssertUnwindSafe};
20use std::ptr;
21use std::sync::OnceLock;
22
23/// `cc_highlight` flag: report offsets in UTF-16 code units instead of bytes.
24pub const CC_UTF16_OFFSETS: u32 = 1 << 0;
25/// `cc_render_html` flag: wrap the output in `<pre class="cc"><code>`.
26pub const CC_HTML_WRAP: u32 = 1 << 1;
27/// `cc_render_html` flag: wrap every line in `<span class="cc-line">`.
28pub const CC_HTML_LINES: u32 = 1 << 2;
29/// `cc_render_ansi` flag: use the 256 color palette instead of 24 bit colors.
30pub const CC_ANSI_256: u32 = 1 << 3;
31
32/// A highlighted span of the input, `start..end`.
33#[repr(C)]
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct CcToken {
36    pub start: u32,
37    pub end: u32,
38    /// A `Kind` as its numeric value.
39    pub kind: u8,
40}
41
42/// How a token kind is drawn.
43#[repr(C)]
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct CcStyle {
46    /// `0xRRGGBB`.
47    pub color: u32,
48    pub bold: u8,
49    pub italic: u8,
50    pub underline: u8,
51}
52
53/// The result of `cc_highlight`. Opaque to C.
54pub struct CcTokens(Vec<CcToken>);
55
56/// NUL terminated copies of static strings, built on first use and kept for
57/// the life of the program so C callers never free them.
58struct CStrings(OnceLock<Vec<CString>>);
59
60impl CStrings {
61    const fn new() -> CStrings {
62        CStrings(OnceLock::new())
63    }
64
65    fn get(&self, index: Option<usize>, init: impl FnOnce() -> Vec<&'static str>) -> *const c_char {
66        let all = self.0.get_or_init(|| init().into_iter().map(|s| CString::new(s).unwrap_or_default()).collect());
67        index.and_then(|i| all.get(i)).map_or(ptr::null(), |s| s.as_ptr())
68    }
69}
70
71/// Runs `f`, returning `fallback` if it panics so nothing unwinds into C.
72fn guard<T>(fallback: T, f: impl FnOnce() -> T) -> T {
73    catch_unwind(AssertUnwindSafe(f)).unwrap_or(fallback)
74}
75
76fn index(i: i32) -> Option<usize> {
77    usize::try_from(i).ok()
78}
79
80fn to_index(i: Option<usize>) -> i32 {
81    i.and_then(|i| i32::try_from(i).ok()).unwrap_or(-1)
82}
83
84fn language_at(i: i32) -> Language {
85    index(i).and_then(|i| Language::all().get(i)).copied().unwrap_or_else(Language::plain)
86}
87
88fn language_index(language: Option<Language>) -> i32 {
89    to_index(language.and_then(|l| Language::all().iter().position(|x| *x == l)))
90}
91
92fn theme_at(i: i32) -> Option<&'static Theme> {
93    index(i).and_then(|i| Theme::all().get(i))
94}
95
96fn theme_or_default(i: i32) -> &'static Theme {
97    theme_at(i).unwrap_or(&Theme::all()[0])
98}
99
100/// Borrows a NUL terminated UTF-8 string.
101unsafe fn str_arg<'a>(s: *const c_char) -> Option<&'a str> {
102    if s.is_null() {
103        return None;
104    }
105    unsafe { CStr::from_ptr(s) }.to_str().ok()
106}
107
108/// Borrows `len` bytes of source code. `None` for invalid UTF-8, input of
109/// 4 GiB or more, or a null pointer with a nonzero length.
110unsafe fn source<'a>(code: *const c_char, len: usize) -> Option<&'a str> {
111    if code.is_null() {
112        return (len == 0).then_some("");
113    }
114    if len > u32::MAX as usize {
115        return None;
116    }
117    std::str::from_utf8(unsafe { std::slice::from_raw_parts(code.cast::<u8>(), len) }).ok()
118}
119
120fn into_c_string(s: String) -> *mut c_char {
121    CString::new(s).map_or(ptr::null_mut(), CString::into_raw)
122}
123
124/// Library version, for example `"0.1.0"`.
125#[no_mangle]
126pub extern "C" fn cc_version() -> *const c_char {
127    concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
128}
129
130/// Number of languages.
131#[no_mangle]
132pub extern "C" fn cc_language_count() -> i32 {
133    to_index(Some(Language::all().len()))
134}
135
136/// Finds a language by id, alias, name or extension. Returns -1 if unknown.
137///
138/// # Safety
139///
140/// `name` must be null or a NUL terminated string.
141#[no_mangle]
142pub unsafe extern "C" fn cc_language_find(name: *const c_char) -> i32 {
143    language_index(unsafe { str_arg(name) }.and_then(Language::find))
144}
145
146/// Guesses the language from a file path. Returns -1 if unknown.
147///
148/// # Safety
149///
150/// `path` must be null or a NUL terminated string.
151#[no_mangle]
152pub unsafe extern "C" fn cc_language_from_path(path: *const c_char) -> i32 {
153    language_index(unsafe { str_arg(path) }.and_then(Language::from_path))
154}
155
156/// Guesses the language from a shebang or similar first line. Returns -1 if unknown.
157///
158/// # Safety
159///
160/// `code` must be null or point to `len` readable bytes.
161#[no_mangle]
162pub unsafe extern "C" fn cc_language_from_content(code: *const c_char, len: usize) -> i32 {
163    language_index(unsafe { source(code, len) }.and_then(Language::from_content))
164}
165
166/// Guesses the language from the content alone, scoring the first few KB
167/// against every language. Opt-in: highlighting never calls it. Returns -1
168/// when unsure or for invalid UTF-8.
169///
170/// # Safety
171///
172/// `code` must be null or point to `len` readable bytes.
173#[no_mangle]
174pub unsafe extern "C" fn cc_language_detect(code: *const c_char, len: usize) -> i32 {
175    let Some(code) = (unsafe { source(code, len) }) else {
176        return -1;
177    };
178    guard(-1, || language_index(Language::detect(code)))
179}
180
181/// Canonical id of a language, for example `"rust"`. NULL if out of range.
182#[no_mangle]
183pub extern "C" fn cc_language_id(language: i32) -> *const c_char {
184    static IDS: CStrings = CStrings::new();
185    IDS.get(index(language), || Language::all().iter().map(|l| l.id()).collect())
186}
187
188/// Display name of a language, for example `"Rust"`. NULL if out of range.
189#[no_mangle]
190pub extern "C" fn cc_language_name(language: i32) -> *const c_char {
191    static NAMES: CStrings = CStrings::new();
192    NAMES.get(index(language), || Language::all().iter().map(|l| l.name()).collect())
193}
194
195/// Highlights `len` bytes of UTF-8 `code`. An unknown language index means
196/// plain text. Returns NULL for invalid UTF-8. Free with `cc_tokens_free`.
197///
198/// # Safety
199///
200/// `code` must be null or point to `len` readable bytes.
201#[no_mangle]
202pub unsafe extern "C" fn cc_highlight(code: *const c_char, len: usize, language: i32, flags: u32) -> *mut CcTokens {
203    let Some(code) = (unsafe { source(code, len) }) else {
204        return ptr::null_mut();
205    };
206    guard(ptr::null_mut(), || {
207        let highlighted = highlight(code, language_at(language));
208        let tokens = if flags & CC_UTF16_OFFSETS != 0 { highlighted.utf16_tokens() } else { highlighted.into_tokens() };
209        let tokens = tokens.iter().map(|t| CcToken { start: t.start, end: t.end, kind: t.kind as u8 }).collect();
210        Box::into_raw(Box::new(CcTokens(tokens)))
211    })
212}
213
214/// Number of tokens. 0 for NULL.
215///
216/// # Safety
217///
218/// `tokens` must be null or come from `cc_highlight` and not be freed.
219#[no_mangle]
220pub unsafe extern "C" fn cc_tokens_len(tokens: *const CcTokens) -> usize {
221    unsafe { tokens.as_ref() }.map_or(0, |t| t.0.len())
222}
223
224/// The tokens, valid until `cc_tokens_free`. NULL for NULL.
225///
226/// # Safety
227///
228/// `tokens` must be null or come from `cc_highlight` and not be freed.
229#[no_mangle]
230pub unsafe extern "C" fn cc_tokens_data(tokens: *const CcTokens) -> *const CcToken {
231    unsafe { tokens.as_ref() }.map_or(ptr::null(), |t| t.0.as_ptr())
232}
233
234/// Frees tokens. NULL is ignored.
235///
236/// # Safety
237///
238/// `tokens` must be null or come from `cc_highlight` and not be freed yet.
239#[no_mangle]
240pub unsafe extern "C" fn cc_tokens_free(tokens: *mut CcTokens) {
241    if !tokens.is_null() {
242        drop(unsafe { Box::from_raw(tokens) });
243    }
244}
245
246/// Lowercase name of a token kind, for example `"keyword"`. NULL if out of range.
247#[no_mangle]
248pub extern "C" fn cc_kind_name(kind: u8) -> *const c_char {
249    static NAMES: CStrings = CStrings::new();
250    NAMES.get(Some(kind.into()), || Kind::ALL.iter().map(|k| k.name()).collect())
251}
252
253/// Number of themes.
254#[no_mangle]
255pub extern "C" fn cc_theme_count() -> i32 {
256    to_index(Some(Theme::all().len()))
257}
258
259/// Finds a theme by name (case insensitive). Returns -1 if unknown.
260///
261/// # Safety
262///
263/// `name` must be null or a NUL terminated string.
264#[no_mangle]
265pub unsafe extern "C" fn cc_theme_find(name: *const c_char) -> i32 {
266    let theme = unsafe { str_arg(name) }.and_then(Theme::find);
267    to_index(theme.and_then(|t| Theme::all().iter().position(|x| ptr::eq(x, t))))
268}
269
270/// Name of a theme, for example `"dracula"`. NULL if out of range.
271#[no_mangle]
272pub extern "C" fn cc_theme_name(theme: i32) -> *const c_char {
273    static NAMES: CStrings = CStrings::new();
274    NAMES.get(index(theme), || Theme::all().iter().map(|t| t.name).collect())
275}
276
277/// 1 for a dark theme, 0 for a light one. Out of range means the default theme.
278#[no_mangle]
279pub extern "C" fn cc_theme_is_dark(theme: i32) -> u8 {
280    theme_or_default(theme).dark.into()
281}
282
283/// Background color as `0xRRGGBB`. Out of range means the default theme.
284#[no_mangle]
285pub extern "C" fn cc_theme_background(theme: i32) -> u32 {
286    theme_or_default(theme).background.to_u32()
287}
288
289/// Default text color as `0xRRGGBB`. Out of range means the default theme.
290#[no_mangle]
291pub extern "C" fn cc_theme_foreground(theme: i32) -> u32 {
292    theme_or_default(theme).foreground.to_u32()
293}
294
295/// Style of a token kind. Out of range themes mean the default theme and
296/// unknown kinds mean plain text.
297#[no_mangle]
298pub extern "C" fn cc_theme_style(theme: i32, kind: u8) -> CcStyle {
299    let style = theme_or_default(theme).style(Kind::from_u8(kind).unwrap_or(Kind::Text));
300    CcStyle {
301        color: style.color.to_u32(),
302        bold: style.bold.into(),
303        italic: style.italic.into(),
304        underline: style.underline.into(),
305    }
306}
307
308/// CSS for the class based HTML, scoped to `selector` (NULL means `".cc"`).
309/// Free with `cc_string_free`.
310///
311/// # Safety
312///
313/// `selector` must be null or a NUL terminated string.
314#[no_mangle]
315pub unsafe extern "C" fn cc_theme_css(theme: i32, selector: *const c_char) -> *mut c_char {
316    let selector = if selector.is_null() { Some(".cc") } else { unsafe { str_arg(selector) } };
317    let Some(selector) = selector else {
318        return ptr::null_mut();
319    };
320    guard(ptr::null_mut(), || into_c_string(theme_or_default(theme).to_css(selector)))
321}
322
323/// Renders HTML. A valid theme index gives inline styles, -1 gives class
324/// names. Takes `CC_HTML_*` flags. Returns NULL for invalid UTF-8 or NUL
325/// bytes in the input. Free with `cc_string_free`.
326///
327/// # Safety
328///
329/// `code` must be null or point to `len` readable bytes.
330#[no_mangle]
331pub unsafe extern "C" fn cc_render_html(
332    code: *const c_char,
333    len: usize,
334    language: i32,
335    theme: i32,
336    flags: u32,
337) -> *mut c_char {
338    let Some(code) = (unsafe { source(code, len) }) else {
339        return ptr::null_mut();
340    };
341    guard(ptr::null_mut(), || {
342        let options = html::Options {
343            theme: theme_at(theme),
344            wrap: flags & CC_HTML_WRAP != 0,
345            lines: flags & CC_HTML_LINES != 0,
346            ..Default::default()
347        };
348        into_c_string(highlight(code, language_at(language)).to_html(&options))
349    })
350}
351
352/// Renders text with ANSI color escapes. Out of range themes mean the
353/// default theme. Takes `CC_ANSI_256`. Returns NULL for invalid UTF-8 or NUL
354/// bytes in the input. Free with `cc_string_free`.
355///
356/// # Safety
357///
358/// `code` must be null or point to `len` readable bytes.
359#[no_mangle]
360pub unsafe extern "C" fn cc_render_ansi(
361    code: *const c_char,
362    len: usize,
363    language: i32,
364    theme: i32,
365    flags: u32,
366) -> *mut c_char {
367    let Some(code) = (unsafe { source(code, len) }) else {
368        return ptr::null_mut();
369    };
370    guard(ptr::null_mut(), || {
371        let mode = if flags & CC_ANSI_256 != 0 { ansi::ColorMode::Ansi256 } else { ansi::ColorMode::TrueColor };
372        into_c_string(highlight(code, language_at(language)).to_ansi(theme_or_default(theme), mode))
373    })
374}
375
376/// Frees a string returned by this library. NULL is ignored.
377///
378/// # Safety
379///
380/// `s` must be null or come from this library and not be freed yet.
381#[no_mangle]
382pub unsafe extern "C" fn cc_string_free(s: *mut c_char) {
383    if !s.is_null() {
384        drop(unsafe { CString::from_raw(s) });
385    }
386}