Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 89 additions & 6 deletions src/typst_core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,22 +33,36 @@ 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 {
pub ptr: *mut u8,
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,
Expand All @@ -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,
Expand Down Expand Up @@ -143,17 +186,32 @@ 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);
}
}
}

/// 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;
}
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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);
Expand Down
34 changes: 19 additions & 15 deletions src/typst_core/tests/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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"
Expand All @@ -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
Expand Down
85 changes: 85 additions & 0 deletions src/typstsharp/Bindings.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,132 @@ internal static unsafe partial class NativeMethods



/// <summary>
/// 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`].
/// </summary>
[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);

/// <summary>
/// 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.
/// </summary>
[DllImport(__DllName, EntryPoint = "free_compiler", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void free_compiler(Compiler* compiler);

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// 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.
/// </summary>
[DllImport(__DllName, EntryPoint = "compile", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern CompileResult compile(Compiler* compiler, byte* format_ptr, float ppi, byte* pdf_standards);

/// <summary>
/// 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.
/// </summary>
[DllImport(__DllName, EntryPoint = "free_compile_result", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void free_compile_result(CompileResult result);

/// <summary>
/// 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.
/// </summary>
[DllImport(__DllName, EntryPoint = "reset_world", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void reset_world();


}

/// <summary>
/// The stateful Typst compilation world, kept alive across compilations so that the incremental
/// cache can be reused.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct Compiler
{
}

/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct Buffer
{
public byte* ptr;
public nuint len;
}

/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct Warning
{
public byte* message_ptr;
public nuint message_len;
}

/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct CompileResult
{
Expand Down
Loading