typst_cffi/
lib.rs

1// Copyright ©2025 The typst-cffi Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5use libc::{self, c_char, c_float, size_t};
6
7use std::ffi::{CStr, CString};
8use std::ptr;
9
10mod compiler;
11mod document;
12
13use crate::compiler::compile;
14use crate::document::Document;
15
16pub struct Context {
17    doc: Result<Document, String>, // internal Typst representation
18    out: *const u8,                // buffer for output document
19    len: usize,                    // length of output document buffer
20    err: *const c_char,            // error message if any.
21}
22
23impl Drop for Context {
24    fn drop(&mut self) {
25        unsafe {
26            libc::free(self.out as *mut libc::c_void);
27            libc::free(self.err as *mut libc::c_void);
28        }
29    }
30}
31
32const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
33
34/// # Safety
35///
36/// typst_cffi_version returns the typst-cffi version "MAJOR.MINOR.PATCH".
37/// The returned data is owned by Typst.
38#[unsafe(no_mangle)]
39pub unsafe extern "C" fn typst_cffi_version() -> *const c_char {
40    VERSION.as_ptr() as *const c_char
41}
42
43/// # Safety
44///
45/// typst_free drops all typst-managed memory for this Context.
46#[unsafe(no_mangle)]
47pub unsafe extern "C" fn typst_free(ptr: *mut Context) {
48    if ptr.is_null() {
49        return;
50    }
51    unsafe {
52        drop(Box::from_raw(ptr));
53    }
54}
55
56/// # Safety
57///
58/// typst_get_err returns the error message from Typst compilation, if any.
59/// The returned message is owned by Typst.
60#[unsafe(no_mangle)]
61pub unsafe extern "C" fn typst_get_err(ptr: *const Context) -> *const c_char {
62    if ptr.is_null() {
63        return ptr::null();
64    }
65
66    let ctx = unsafe { &*ptr };
67    ctx.err
68}
69
70/// # Safety
71///
72/// typst_get_buf returns the document produced by Typst compilation, if any.
73/// The returned data is owned by Typst.
74#[unsafe(no_mangle)]
75pub unsafe extern "C" fn typst_get_buf(ptr: *const Context, len: *mut size_t) -> *const u8 {
76    if ptr.is_null() {
77        unsafe {
78            *len = 0;
79        }
80        return ptr::null();
81    }
82
83    let ctx = unsafe { &*ptr };
84    unsafe {
85        *len = ctx.len;
86    }
87    ctx.out
88}
89
90/// # Safety
91///
92/// typst_get_npages returns the number of pages in the document produced by Typst compilation, if any.
93#[unsafe(no_mangle)]
94pub unsafe extern "C" fn typst_get_npages(ptr: *const Context) -> size_t {
95    if ptr.is_null() {
96        return 0;
97    }
98
99    let ctx = unsafe { &*ptr };
100
101    match &ctx.doc {
102        Err(_) => 0,
103        Ok(doc) => doc.npages(),
104    }
105}
106
107/// # Safety
108///
109/// typst_compile compiles the provided Typst document.
110/// The returned Context is owned by Typst.
111#[unsafe(no_mangle)]
112pub unsafe extern "C" fn typst_compile(src: *const c_char) -> *mut Context {
113    if src.is_null() {
114        return Box::into_raw(Box::new(Context {
115            doc: Err("no document".to_string()),
116            out: ptr::null(),
117            len: 0,
118            err: CString::new("NULL pointer to document".to_string())
119                .unwrap()
120                .into_raw(),
121        }));
122    }
123
124    let c_str = unsafe { CStr::from_ptr(src) };
125    let v_str = match c_str.to_str() {
126        Ok(s) => s,
127        Err(err) => {
128            return Box::into_raw(Box::new(Context {
129                doc: Err(err.to_string()),
130                out: ptr::null(),
131                len: 0,
132                err: CString::new(err.to_string()).unwrap().into_raw(),
133            }));
134        }
135    };
136
137    match compile(v_str.to_string()) {
138        Err(err) => Box::into_raw(Box::new(Context {
139            doc: Err(err.clone()),
140            out: ptr::null(),
141            len: 0,
142            err: CString::new(err.to_string()).unwrap().into_raw(),
143        })),
144        Ok(doc) => Box::into_raw(Box::new(Context {
145            doc: Ok(doc),
146            out: ptr::null(),
147            len: 0,
148            err: ptr::null(),
149        })),
150    }
151}
152
153/// # Safety
154///
155/// typst_compile_pdf compiles the provided Typst document to PDF.
156/// The returned Context is owned by Typst.
157#[unsafe(no_mangle)]
158pub unsafe extern "C" fn typst_compile_pdf(ptr: *mut Context) {
159    if ptr.is_null() {
160        return;
161    }
162
163    let ctx = unsafe { &mut *ptr };
164    unsafe {
165        libc::free(ctx.out as *mut libc::c_void);
166        libc::free(ctx.err as *mut libc::c_void);
167    }
168
169    match &ctx.doc {
170        Err(err) => {
171            ctx.err = CString::new(err.to_string()).unwrap().into_raw();
172        }
173        Ok(doc) => match doc.compile_pdf() {
174            Ok(buf) => {
175                let len = buf.len();
176                let ptr = unsafe { libc::malloc(len) as *mut u8 };
177                if !ptr.is_null() {
178                    unsafe {
179                        ptr.copy_from_nonoverlapping(buf.as_ptr(), len);
180                    }
181                }
182                ctx.len = len;
183                ctx.out = ptr;
184            }
185            Err(err) => {
186                ctx.err = CString::new(err.to_string()).unwrap().into_raw();
187            }
188        },
189    }
190}
191
192/// # Safety
193///
194/// typst_compile_png compiles the provided Typst document to PNG.
195/// The returned Context is owned by Typst.
196#[unsafe(no_mangle)]
197pub unsafe extern "C" fn typst_compile_png(ptr: *mut Context, page: size_t, ppi: c_float) {
198    if ptr.is_null() {
199        return;
200    }
201
202    let ctx = unsafe { &mut *ptr };
203    unsafe {
204        libc::free(ctx.out as *mut libc::c_void);
205        libc::free(ctx.err as *mut libc::c_void);
206    }
207
208    match &ctx.doc {
209        Err(err) => {
210            ctx.err = CString::new(err.to_string()).unwrap().into_raw();
211        }
212        Ok(doc) => match doc.compile_png(page, ppi) {
213            Ok(buf) => {
214                let len = buf.len();
215                let ptr = unsafe { libc::malloc(len) as *mut u8 };
216                if !ptr.is_null() {
217                    unsafe {
218                        ptr.copy_from_nonoverlapping(buf.as_ptr(), len);
219                    }
220                }
221                ctx.len = len;
222                ctx.out = ptr;
223            }
224            Err(err) => {
225                ctx.err = CString::new(err.to_string()).unwrap().into_raw();
226            }
227        },
228    }
229}
230
231/// # Safety
232///
233/// typst_compile_svg compiles the provided Typst document to SVG.
234/// The returned Context is owned by Typst.
235#[unsafe(no_mangle)]
236pub unsafe extern "C" fn typst_compile_svg(ptr: *mut Context) {
237    if ptr.is_null() {
238        return;
239    }
240
241    let ctx = unsafe { &mut *ptr };
242    unsafe {
243        libc::free(ctx.out as *mut libc::c_void);
244        libc::free(ctx.err as *mut libc::c_void);
245    }
246
247    match &ctx.doc {
248        Err(err) => {
249            ctx.err = CString::new(err.to_string()).unwrap().into_raw();
250        }
251        Ok(doc) => match doc.compile_svg() {
252            Ok(buf) => {
253                let len = buf.len();
254                let ptr = unsafe { libc::malloc(len) as *mut u8 };
255                if !ptr.is_null() {
256                    unsafe {
257                        ptr.copy_from_nonoverlapping(buf.as_ptr(), len);
258                    }
259                }
260                ctx.len = len;
261                ctx.out = ptr;
262            }
263            Err(err) => {
264                ctx.err = CString::new(err.to_string()).unwrap().into_raw();
265            }
266        },
267    }
268}