typst-cffi 0.6.0

FFI to Typst
Documentation
// Copyright ©2025 The typst-cffi Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

use libc::{self, c_char, c_float, size_t};

use std::ffi::{CStr, CString};
use std::ptr;

mod compiler;
mod document;

use crate::compiler::compile;
use crate::document::Document;

pub struct Context {
    doc: Result<Document, String>, // internal Typst representation
    out: *const u8,                // buffer for output document
    len: usize,                    // length of output document buffer
    err: *const c_char,            // error message if any.
}

impl Drop for Context {
    fn drop(&mut self) {
        unsafe {
            libc::free(self.out as *mut libc::c_void);
            libc::free(self.err as *mut libc::c_void);
        }
    }
}

const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");

/// # Safety
///
/// typst_cffi_version returns the typst-cffi version "MAJOR.MINOR.PATCH".
/// The returned data is owned by Typst.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_cffi_version() -> *const c_char {
    VERSION.as_ptr() as *const c_char
}

/// # Safety
///
/// typst_free drops all typst-managed memory for this Context.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_free(ptr: *mut Context) {
    if ptr.is_null() {
        return;
    }
    unsafe {
        drop(Box::from_raw(ptr));
    }
}

/// # Safety
///
/// typst_get_err returns the error message from Typst compilation, if any.
/// The returned message is owned by Typst.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_get_err(ptr: *const Context) -> *const c_char {
    if ptr.is_null() {
        return ptr::null();
    }

    let ctx = unsafe { &*ptr };
    ctx.err
}

/// # Safety
///
/// typst_get_buf returns the document produced by Typst compilation, if any.
/// The returned data is owned by Typst.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_get_buf(ptr: *const Context, len: *mut size_t) -> *const u8 {
    if ptr.is_null() {
        unsafe {
            *len = 0;
        }
        return ptr::null();
    }

    let ctx = unsafe { &*ptr };
    unsafe {
        *len = ctx.len;
    }
    ctx.out
}

/// # Safety
///
/// typst_get_npages returns the number of pages in the document produced by Typst compilation, if any.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_get_npages(ptr: *const Context) -> size_t {
    if ptr.is_null() {
        return 0;
    }

    let ctx = unsafe { &*ptr };

    match &ctx.doc {
        Err(_) => 0,
        Ok(doc) => doc.npages(),
    }
}

/// # Safety
///
/// typst_compile compiles the provided Typst document.
/// The returned Context is owned by Typst.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_compile(src: *const c_char) -> *mut Context {
    if src.is_null() {
        return Box::into_raw(Box::new(Context {
            doc: Err("no document".to_string()),
            out: ptr::null(),
            len: 0,
            err: CString::new("NULL pointer to document".to_string())
                .unwrap()
                .into_raw(),
        }));
    }

    let c_str = unsafe { CStr::from_ptr(src) };
    let v_str = match c_str.to_str() {
        Ok(s) => s,
        Err(err) => {
            return Box::into_raw(Box::new(Context {
                doc: Err(err.to_string()),
                out: ptr::null(),
                len: 0,
                err: CString::new(err.to_string()).unwrap().into_raw(),
            }));
        }
    };

    match compile(v_str.to_string()) {
        Err(err) => Box::into_raw(Box::new(Context {
            doc: Err(err.clone()),
            out: ptr::null(),
            len: 0,
            err: CString::new(err.to_string()).unwrap().into_raw(),
        })),
        Ok(doc) => Box::into_raw(Box::new(Context {
            doc: Ok(doc),
            out: ptr::null(),
            len: 0,
            err: ptr::null(),
        })),
    }
}

/// # Safety
///
/// typst_compile_pdf compiles the provided Typst document to PDF.
/// The returned Context is owned by Typst.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_compile_pdf(ptr: *mut Context) {
    if ptr.is_null() {
        return;
    }

    let ctx = unsafe { &mut *ptr };
    unsafe {
        libc::free(ctx.out as *mut libc::c_void);
        libc::free(ctx.err as *mut libc::c_void);
    }

    match &ctx.doc {
        Err(err) => {
            ctx.err = CString::new(err.to_string()).unwrap().into_raw();
        }
        Ok(doc) => match doc.compile_pdf() {
            Ok(buf) => {
                let len = buf.len();
                let ptr = unsafe { libc::malloc(len) as *mut u8 };
                if !ptr.is_null() {
                    unsafe {
                        ptr.copy_from_nonoverlapping(buf.as_ptr(), len);
                    }
                }
                ctx.len = len;
                ctx.out = ptr;
            }
            Err(err) => {
                ctx.err = CString::new(err.to_string()).unwrap().into_raw();
            }
        },
    }
}

/// # Safety
///
/// typst_compile_png compiles the provided Typst document to PNG.
/// The returned Context is owned by Typst.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_compile_png(ptr: *mut Context, page: size_t, ppi: c_float) {
    if ptr.is_null() {
        return;
    }

    let ctx = unsafe { &mut *ptr };
    unsafe {
        libc::free(ctx.out as *mut libc::c_void);
        libc::free(ctx.err as *mut libc::c_void);
    }

    match &ctx.doc {
        Err(err) => {
            ctx.err = CString::new(err.to_string()).unwrap().into_raw();
        }
        Ok(doc) => match doc.compile_png(page, ppi) {
            Ok(buf) => {
                let len = buf.len();
                let ptr = unsafe { libc::malloc(len) as *mut u8 };
                if !ptr.is_null() {
                    unsafe {
                        ptr.copy_from_nonoverlapping(buf.as_ptr(), len);
                    }
                }
                ctx.len = len;
                ctx.out = ptr;
            }
            Err(err) => {
                ctx.err = CString::new(err.to_string()).unwrap().into_raw();
            }
        },
    }
}

/// # Safety
///
/// typst_compile_svg compiles the provided Typst document to SVG.
/// The returned Context is owned by Typst.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn typst_compile_svg(ptr: *mut Context) {
    if ptr.is_null() {
        return;
    }

    let ctx = unsafe { &mut *ptr };
    unsafe {
        libc::free(ctx.out as *mut libc::c_void);
        libc::free(ctx.err as *mut libc::c_void);
    }

    match &ctx.doc {
        Err(err) => {
            ctx.err = CString::new(err.to_string()).unwrap().into_raw();
        }
        Ok(doc) => match doc.compile_svg() {
            Ok(buf) => {
                let len = buf.len();
                let ptr = unsafe { libc::malloc(len) as *mut u8 };
                if !ptr.is_null() {
                    unsafe {
                        ptr.copy_from_nonoverlapping(buf.as_ptr(), len);
                    }
                }
                ctx.len = len;
                ctx.out = ptr;
            }
            Err(err) => {
                ctx.err = CString::new(err.to_string()).unwrap().into_raw();
            }
        },
    }
}