docs: write down the FFI ownership contract - #36
Merged
evolvedlight merged 1 commit intoSep 6, 2026
Conversation
Every exported function takes or returns raw pointers, so the ownership rules are the contract between the crate and its caller, but none of them were stated anywhere in the source. The managed side depends on four of them in particular: the buffers in a CompileResult are independent allocations owned by the result, they do not borrow from the compiler that produced them, they stay valid until free_compile_result whatever else happens in between, and that call has to happen exactly once. Those hold today; nothing said so, and nothing would have caught a refactor that changed them. A module-level doc states the rules that span calls, and each exported function and repr(C) type carries a description and, where it dereferences caller-supplied pointers, a Safety section. create_compiler, free_compiler, set_sys_inputs, compile and free_compile_result become unsafe extern "C" fn. All five dereference raw pointers they are handed, and free_compile_result frees the same allocations twice if it is called twice. reset_world takes no pointers and stays safe. csbindgen lifts the doc comments into the generated bindings, so the contract also reaches the managed side. No signature changes: unsafe is a Rust-side marker and does not touch the ABI.
msallin
force-pushed
the
docs/ffi-ownership-contract
branch
from
August 31, 2026 20:24
498dfa4 to
e34d597
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the part of #29 that is still open.
The allocation fix and the missing null check both landed in #33:
vec_to_rawnow goes throughinto_boxed_slice, so the deallocation layout matches by construction, andcompile_internalrejects a null compiler. What was left is the third item: the ownership contract was nowhere in the source, andfree_compile_resultwas a safeextern "C" fneven though calling it twice frees the same allocations twice.What this adds
A module-level doc stating the rules a caller has to rely on across calls, and a
# Safetysection plus a description on each exported function and each#[repr(C)]type. The four guarantees the issue asked to have written down are now oncompileandCompileResult: the buffers are independent allocations owned by the result, they do not borrow from the compiler, they stay valid untilfree_compile_resultwhatever else happens in between, and that call must happen exactly once.create_compiler,free_compiler,set_sys_inputs,compileandfree_compile_resultare nowunsafe extern "C" fn.reset_worldtakes no pointers and stays safe.Two things worth knowing
This is slightly wider than the issue, which named only
free_compile_result. Every one of those five dereferences a caller-supplied raw pointer, so marking one and not the others would have been arbitrary. Happy to narrow it tofree_compile_resultalone if you would rather keep the diff minimal.Bindings.g.csis regenerated and included. csbindgen lifts Rust doc comments into C# XML summaries, so the contract now shows up in IntelliSense on the managed side, which is where it actually gets relied on. No signature changed:unsafeis a Rust-side marker and does not touch the ABI, so everyDllImportis byte-for-byte what it was.Testing
cargo testgreen, and the managed suite green on linux-x64 against alibtypst_core.sobuilt from this branch. No behaviour change, so the value here is entirely in what a future refactor will be told before it breaks something.