diff --git a/src/typst_core/src/lib.rs b/src/typst_core/src/lib.rs
index 5b10f20..b8feac4 100644
--- a/src/typst_core/src/lib.rs
+++ b/src/typst_core/src/lib.rs
@@ -1,3 +1,22 @@
+//! The C ABI that the managed `typstsharp` package binds to.
+//!
+//! Every exported function takes or returns raw pointers, so the ownership rules are the contract
+//! between this crate and its caller. They are stated on each function; the parts a caller has to
+//! rely on across calls are:
+//!
+//! - A `Compiler` is created by [`create_compiler`] and lives until [`free_compiler`]. It is not
+//! synchronised: one compiler must not be used from two threads at once.
+//! - A [`CompileResult`] owns its buffers, warning messages and error message. Each is an
+//! independent heap allocation, and none of them borrows from the compiler that produced them.
+//! - Those allocations stay valid until [`free_compile_result`] is called on the result that owns
+//! them, whatever else happens in between: further [`compile`] calls, [`set_sys_inputs`],
+//! [`free_compiler`] on the originating compiler, or [`reset_world`].
+//! - [`free_compile_result`] may be called from any thread, and must be called exactly once per
+//! result. Calling it twice frees the same allocations twice.
+//!
+//! Callers may therefore hold a result and read from its buffers for as long as they like, which is
+//! what lets the managed side hand out the rendered document without copying it.
+
#![allow(non_camel_case_types)]
use std::ffi::{CStr, c_char};
use std::path::PathBuf;
@@ -14,9 +33,14 @@ use typst::{World, WorldExt};
use typst_layout::PagedDocument;
use world::SystemWorld;
-// This represents the stateful compiler in Rust.
+/// The stateful Typst compilation world, kept alive across compilations so that the incremental
+/// cache can be reused.
pub struct Compiler(SystemWorld);
+/// One rendered output: the whole document for PDF export, one page for PNG and SVG.
+///
+/// The bytes are owned by the [`CompileResult`] that contains this buffer and are freed by
+/// [`free_compile_result`]. They are not NUL-terminated; `len` is the only length.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Buffer {
@@ -24,12 +48,21 @@ pub struct Buffer {
pub len: usize,
}
+/// One warning emitted by a compilation that nevertheless succeeded.
+///
+/// `message_ptr` is UTF-8 and is not NUL-terminated, so it must be read with `message_len`. A
+/// message may itself contain NUL bytes, because Typst diagnostics quote the source.
#[repr(C)]
pub struct Warning {
pub message_ptr: *mut u8,
pub message_len: usize,
}
+/// The outcome of one [`compile`] call, owning everything it points at.
+///
+/// Either `error_ptr` is non-null and the compilation failed, or it is null and `buffers` holds the
+/// rendered output. `warnings` may be populated in both cases. Every allocation reachable from here
+/// is released by [`free_compile_result`], and by nothing else.
#[repr(C)]
pub struct CompileResult {
pub buffers: *mut Buffer,
@@ -53,8 +86,18 @@ impl Default for CompileResult {
}
}
+/// Creates a compiler that reads its document either from `input_path` or from `input_source`.
+///
+/// # Safety
+///
+/// `root`, `input_path`, `package_path` and `sys_inputs` must be null or NUL-terminated strings,
+/// `font_paths` must be null or point to `font_paths_len` such strings, and `input_source` must be
+/// null or point to `input_source_len` bytes. Unlike the others, the source is passed with an
+/// explicit length and may contain NUL bytes. All of them need only stay valid for the duration of
+/// the call. The returned compiler is owned by the caller and must be released with
+/// [`free_compiler`].
#[unsafe(no_mangle)]
-pub extern "C" fn create_compiler(
+pub unsafe extern "C" fn create_compiler(
root: *const c_char,
input_path: *const c_char,
input_source: *const u8,
@@ -143,8 +186,16 @@ pub extern "C" fn create_compiler(
}
}
+/// Releases a compiler created by [`create_compiler`]. A null pointer is ignored.
+///
+/// Results previously returned by [`compile`] are unaffected: they own their memory and stay valid.
+///
+/// # Safety
+///
+/// `compiler` must be null or a pointer returned by [`create_compiler`] that has not already been
+/// freed, and no other thread may be using it.
#[unsafe(no_mangle)]
-pub extern "C" fn free_compiler(compiler: *mut Compiler) {
+pub unsafe extern "C" fn free_compiler(compiler: *mut Compiler) {
if !compiler.is_null() {
unsafe {
let _ = Box::from_raw(compiler);
@@ -152,8 +203,15 @@ pub extern "C" fn free_compiler(compiler: *mut Compiler) {
}
}
+/// Replaces the `sys.inputs` dictionary the next compilation will see. Returns `false` if the
+/// compiler is null, the JSON does not parse, or Typst rejects the dictionary.
+///
+/// # Safety
+///
+/// `compiler` must be null or a live pointer from [`create_compiler`], and `sys_inputs` must be
+/// null or a NUL-terminated JSON object that stays valid for the duration of the call.
#[unsafe(no_mangle)]
-pub extern "C" fn set_sys_inputs(compiler: *mut Compiler, sys_inputs: *const c_char) -> bool {
+pub unsafe extern "C" fn set_sys_inputs(compiler: *mut Compiler, sys_inputs: *const c_char) -> bool {
if compiler.is_null() {
return false;
}
@@ -354,8 +412,23 @@ fn compile_internal(
}
}
+/// Compiles the document to `format`, which is one of `pdf`, `png` or `svg`.
+///
+/// The returned [`CompileResult`] owns its buffers and messages. They do not borrow from
+/// `compiler`, so they outlive further compilations, [`set_sys_inputs`], [`reset_world`] and even
+/// [`free_compiler`] on the compiler that produced them. The caller must pass the result to
+/// [`free_compile_result`] exactly once.
+///
+/// A panic inside Typst is caught and reported as an error result rather than unwinding across the
+/// ABI boundary.
+///
+/// # Safety
+///
+/// `compiler` must be null or a live pointer from [`create_compiler`] that no other thread is
+/// using. `format_ptr` and `pdf_standards` must be null or NUL-terminated strings that stay valid
+/// for the duration of the call.
#[unsafe(no_mangle)]
-pub extern "C" fn compile(
+pub unsafe extern "C" fn compile(
compiler: *mut Compiler,
format_ptr: *const std::os::raw::c_char,
ppi: f32,
@@ -380,8 +453,16 @@ pub extern "C" fn compile(
}
}
+/// Releases every allocation owned by a [`CompileResult`]: the buffers, the warning messages and
+/// the error message. May be called from any thread.
+///
+/// # Safety
+///
+/// `result` must be a value returned by [`compile`] that has not already been passed to this
+/// function, and nothing may read from its buffers afterwards. Calling this twice on the same
+/// result frees the same allocations twice.
#[unsafe(no_mangle)]
-pub extern "C" fn free_compile_result(result: CompileResult) {
+pub unsafe extern "C" fn free_compile_result(result: CompileResult) {
unsafe {
if !result.buffers.is_null() {
let buffers = Box::from_raw(std::ptr::slice_from_raw_parts_mut(
@@ -407,6 +488,8 @@ pub extern "C" fn free_compile_result(result: CompileResult) {
}
}
+/// Trims the process-global incremental compilation cache. It holds no references to any
+/// [`CompileResult`], so trimming it never invalidates output the caller is still holding.
#[unsafe(no_mangle)]
pub extern "C" fn reset_world() {
comemo::evict(10);
diff --git a/src/typst_core/tests/compile.rs b/src/typst_core/tests/compile.rs
index 9a6f72f..2fb69cc 100644
--- a/src/typst_core/tests/compile.rs
+++ b/src/typst_core/tests/compile.rs
@@ -8,18 +8,20 @@ fn compiler_for(source: &[u8]) -> *mut Compiler {
let root = CString::new(".").unwrap();
let sys_inputs = CString::new("{}").unwrap();
- let compiler = create_compiler(
- root.as_ptr(),
- std::ptr::null(),
- source.as_ptr(),
- source.len(),
- std::ptr::null::<*const c_char>(),
- 0,
- std::ptr::null(),
- sys_inputs.as_ptr(),
- true,
- true,
- );
+ let compiler = unsafe {
+ create_compiler(
+ root.as_ptr(),
+ std::ptr::null(),
+ source.as_ptr(),
+ source.len(),
+ std::ptr::null::<*const c_char>(),
+ 0,
+ std::ptr::null(),
+ sys_inputs.as_ptr(),
+ true,
+ true,
+ )
+ };
assert!(!compiler.is_null(), "failed to create compiler");
compiler
}
@@ -28,7 +30,7 @@ fn compiler_for(source: &[u8]) -> *mut Compiler {
fn compile_expecting_error(source: &[u8]) -> String {
let compiler = compiler_for(source);
- let result = compile(compiler, std::ptr::null(), 96.0, std::ptr::null());
+ let result = unsafe { compile(compiler, std::ptr::null(), 96.0, std::ptr::null()) };
assert!(
!result.error_ptr.is_null(),
"invalid document compiled without an error"
@@ -39,8 +41,10 @@ fn compile_expecting_error(source: &[u8]) -> String {
String::from_utf8_lossy(slice).into_owned()
};
- free_compile_result(result);
- free_compiler(compiler);
+ unsafe {
+ free_compile_result(result);
+ free_compiler(compiler);
+ }
dbg!(&error);
error
diff --git a/src/typstsharp/Bindings.g.cs b/src/typstsharp/Bindings.g.cs
index 23adc63..397153a 100644
--- a/src/typstsharp/Bindings.g.cs
+++ b/src/typstsharp/Bindings.g.cs
@@ -18,33 +18,105 @@ internal static unsafe partial class NativeMethods
+ ///
+ /// Creates a compiler that reads its document either from `input_path` or from `input_source`.
+ ///
+ /// # Safety
+ ///
+ /// `root`, `input_path`, `package_path` and `sys_inputs` must be null or NUL-terminated strings,
+ /// `font_paths` must be null or point to `font_paths_len` such strings, and `input_source` must be
+ /// null or point to `input_source_len` bytes. Unlike the others, the source is passed with an
+ /// explicit length and may contain NUL bytes. All of them need only stay valid for the duration of
+ /// the call. The returned compiler is owned by the caller and must be released with
+ /// [`free_compiler`].
+ ///
[DllImport(__DllName, EntryPoint = "create_compiler", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern Compiler* create_compiler(byte* root, byte* input_path, byte* input_source, nuint input_source_len, byte** font_paths, nuint font_paths_len, byte* package_path, byte* sys_inputs, [MarshalAs(UnmanagedType.U1)] bool ignore_system_fonts, [MarshalAs(UnmanagedType.U1)] bool ignore_system_packages);
+ ///
+ /// Releases a compiler created by [`create_compiler`]. A null pointer is ignored.
+ ///
+ /// Results previously returned by [`compile`] are unaffected: they own their memory and stay valid.
+ ///
+ /// # Safety
+ ///
+ /// `compiler` must be null or a pointer returned by [`create_compiler`] that has not already been
+ /// freed, and no other thread may be using it.
+ ///
[DllImport(__DllName, EntryPoint = "free_compiler", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void free_compiler(Compiler* compiler);
+ ///
+ /// Replaces the `sys.inputs` dictionary the next compilation will see. Returns `false` if the
+ /// compiler is null, the JSON does not parse, or Typst rejects the dictionary.
+ ///
+ /// # Safety
+ ///
+ /// `compiler` must be null or a live pointer from [`create_compiler`], and `sys_inputs` must be
+ /// null or a NUL-terminated JSON object that stays valid for the duration of the call.
+ ///
[DllImport(__DllName, EntryPoint = "set_sys_inputs", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.U1)]
internal static extern bool set_sys_inputs(Compiler* compiler, byte* sys_inputs);
+ ///
+ /// Compiles the document to `format`, which is one of `pdf`, `png` or `svg`.
+ ///
+ /// The returned [`CompileResult`] owns its buffers and messages. They do not borrow from
+ /// `compiler`, so they outlive further compilations, [`set_sys_inputs`], [`reset_world`] and even
+ /// [`free_compiler`] on the compiler that produced them. The caller must pass the result to
+ /// [`free_compile_result`] exactly once.
+ ///
+ /// A panic inside Typst is caught and reported as an error result rather than unwinding across the
+ /// ABI boundary.
+ ///
+ /// # Safety
+ ///
+ /// `compiler` must be null or a live pointer from [`create_compiler`] that no other thread is
+ /// using. `format_ptr` and `pdf_standards` must be null or NUL-terminated strings that stay valid
+ /// for the duration of the call.
+ ///
[DllImport(__DllName, EntryPoint = "compile", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern CompileResult compile(Compiler* compiler, byte* format_ptr, float ppi, byte* pdf_standards);
+ ///
+ /// Releases every allocation owned by a [`CompileResult`]: the buffers, the warning messages and
+ /// the error message. May be called from any thread.
+ ///
+ /// # Safety
+ ///
+ /// `result` must be a value returned by [`compile`] that has not already been passed to this
+ /// function, and nothing may read from its buffers afterwards. Calling this twice on the same
+ /// result frees the same allocations twice.
+ ///
[DllImport(__DllName, EntryPoint = "free_compile_result", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void free_compile_result(CompileResult result);
+ ///
+ /// Trims the process-global incremental compilation cache. It holds no references to any
+ /// [`CompileResult`], so trimming it never invalidates output the caller is still holding.
+ ///
[DllImport(__DllName, EntryPoint = "reset_world", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void reset_world();
}
+ ///
+ /// The stateful Typst compilation world, kept alive across compilations so that the incremental
+ /// cache can be reused.
+ ///
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct Compiler
{
}
+ ///
+ /// One rendered output: the whole document for PDF export, one page for PNG and SVG.
+ ///
+ /// The bytes are owned by the [`CompileResult`] that contains this buffer and are freed by
+ /// [`free_compile_result`]. They are not NUL-terminated; `len` is the only length.
+ ///
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct Buffer
{
@@ -52,6 +124,12 @@ internal unsafe partial struct Buffer
public nuint len;
}
+ ///
+ /// One warning emitted by a compilation that nevertheless succeeded.
+ ///
+ /// `message_ptr` is UTF-8 and is not NUL-terminated, so it must be read with `message_len`. A
+ /// message may itself contain NUL bytes, because Typst diagnostics quote the source.
+ ///
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct Warning
{
@@ -59,6 +137,13 @@ internal unsafe partial struct Warning
public nuint message_len;
}
+ ///
+ /// The outcome of one [`compile`] call, owning everything it points at.
+ ///
+ /// Either `error_ptr` is non-null and the compilation failed, or it is null and `buffers` holds the
+ /// rendered output. `warnings` may be populated in both cases. Every allocation reachable from here
+ /// is released by [`free_compile_result`], and by nothing else.
+ ///
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct CompileResult
{