How the LibPETSc wrappers are generated

Read this before touching anything under src/autowrapped/ or wrapping/generator/. It is written for the next person (or agent) who has to regenerate the low-level wrappers for a new PETSc release, or fix a wrapper that is wrong.

Who runs this

Only a maintainer, once per PETSc release. Users of PETSc.jl never run the generator: the package ships the generated src/autowrapped/ files and needs neither Python, nor a PETSc source tree, nor anything under wrapping/ at run time. The .github/workflows/wrappers.yml job exists to catch hand edits of generated files, not to generate anything for users.

The one rule

Never edit files in src/autowrapped/ by hand. They are generated, and the next regeneration overwrites them. Every fix goes into wrapping/generator/ (a rule, an override, or the generator code) and is then regenerated. The history of this package shows what happens otherwise: four PRs of hand fixes were silently lost the moment someone reran the old script.

Layout

wrapping/
  WRAPPING.md            this file
  generator/             the generator (a Julia project, deps: JSON3, TOML)
    generate.jl            entry point
    getapi_dump.py         runs PETSc's getAPI.py and writes an API snapshot (JSON)
    api/petsc-X.Y.Z.json.gz  API snapshots (gzipped, reproducible; one per PETSc version wrapped)
    rules/                 declarative rules (TOML), the place for fixes
      files.toml             class -> output file, exclusions, include order
      types.toml             type maps, handle types, keyword renames, string-enum overrides
      args.toml              per-function, per-argument overrides (hand-maintained)
      args_mined.toml        the same, mined once from the old hand-edited wrappers (do not edit)
      ownership.toml         the `Get` functions whose returned handles the caller owns
    overrides/NAME.jl      verbatim replacement for one wrapper (last resort)
    prologue.jl            hand-written head of petsc_library.jl (handle structs, MPI, ...)
    petscarray.jl          hand-written PetscArray type (copied verbatim)
    structs.jl             hand-maintained struct_wrappers.jl (copied verbatim)
    petscbool.jl           hand-written PetscBool primitive type (appended to typedefs)
    src/                   generator code (see below)
    golden_diff.jl         compare two autowrapped directories function by function
    bootstrap_rules.jl     mine rules from a hand-edited autowrapped directory (one-off)
    make_overrides.jl      copy functions from an autowrapped directory into overrides/

Pipeline

PETSc source tree ──getapi_dump.py──▶ api/petsc-X.Y.Z.json       (functions, args, enums, structs)
PETSc source tree ──docs.jl─────────▶ manual-page index          (one pass over src/**/*.c)
snapshot + index + rules + overrides ──generate.jl──▶ src/autowrapped/*.jl

Run it:

python3 wrapping/generator/getapi_dump.py /path/to/petsc-X.Y.Z wrapping/generator/api/petsc-X.Y.Z.json.gz
julia --project=wrapping/generator wrapping/generator/generate.jl \
      --petsc-dir /path/to/petsc-X.Y.Z --api wrapping/generator/api/petsc-X.Y.Z.json.gz

Only the source tree is needed (no configure, no build): the 16 MB release tarball from https://web.cels.anl.gov/projects/petsc/download/release-snapshots/petsc-X.Y.Z.tar.gz. A full run takes a few seconds. --out DIR writes somewhere else than src/autowrapped.

getAPI.py lives in config/utils/ up to PETSc 3.24 and in lib/petsc/bin/ from 3.25; it needs the current directory to be the PETSc tree in both cases. getapi_dump.py handles both. The snapshot is deterministic (sorted keys), so two dumps of one tree are identical and two releases can be diffed.

Generator code (generator/src/)

filedoes
api.jlloads the JSON snapshot into API (functions, classes, enums, senums, typedefs, structs)
docs.jlindexes the /*@ ... @*/ manual pages, extracts Input/Output parameter lists, cleans the text into a Julia docstring, finds the manual section (DMPlex, PC, ...) from the makefile next to the C file
types.jlRules (from the TOML files), C -> Julia type mapping, abstract_arg_type
classify.jldecides for every argument what kind it is and emits its code fragments
render.jlwrites docstring + untyped stub + @for_petsc method for one function
support.jlenums, string enums, typedefs, opaque type declarations, version file, petsc_library.jl
driver.jlfile planning, overrides, and the generate() entry point
blocks.jlsplits autowrapped files into named blocks and categorises differences (used by the tools)

What decides input vs output

  1. The manual page. If the argument is listed under Input Parameters it is an input; if under Output Parameters (and it is a pointer) it is an output. This is the primary rule.
  2. Otherwise a single pointer (T *x) is guessed to be an output, unless the function is not a Create/Duplicate/*Type* function and T is not a scalar/enum/string type, or the function name contains Restore or Copy (the old generator's heuristics).
  3. [FunctionName.arg] direction = "in" | "out" | "inout" in args.toml overrides both; "inout" is for a T *x scalar PETSc reads and then overwrites (PetscSplitOwnership, PetscSortRemoveDupsInt, the nmax of PetscOptionsGet*Array): it is an argument and is returned. A non-const T *x scalar is otherwise always an output, so such functions must have this rule or PETSc reads an uninitialised value.

Argument kinds (how each C shape is wrapped)

C shapeJulia signatureccallbody
T x scalarx::TT
T *x output, scalar/enumreturn x::TPtr{T}x_ = Ref{T}() ... x = x_[]
XType *x output (string enum)return x::StringPtr{XType}unsafe_string, "" when NULL
XType x input (string enum)x::StringXTypethe String converts in the ccall; senums_wrappers.jl also defines the registered names (const PCMG = "mg")
const char *x[] outputreturn x::StringPtr{Ptr{Cchar}}PETSc-owned string (PetscObjectGetType); a non-const char *x[] output stays a raw pointer (caller-allocated array of strings)
Vec x (handle)x::AbstractPetscVecCVecvia unsafe_convert
Vec *x outputreturn x::PetscVecPtr{CVec}x_ = Ref{CVec}() ... x = PetscVec(x_[], petsclib)
Vec *x in XDestroyx::AbstractPetscVecPtr{CVec}x_ = Ref(x.ptr) ... x.ptr = C_NULL
Vec *x not documented as output, not Destroyx::AbstractPetscVecPtr{CVec}x_ = Ref(x.ptr) ... x.ptr = x_[] (write-back)
Opaque *x in XDestroyx::Union{X, Ref{X}}Ptr{X}x_ = x isa Base.RefValue ? x : Ref{X}(x)
const T *x / T x[] inputx::Vector{T}Ptr{T}
const char *s / char s[] inputs::StringPtr{Cchar}
char buf[], size_t lenbuf::Vector{Cchar}, len::Csize_tPtr{Cchar}, Csize_tcaller allocates
char **s outputreturn s::StringPtr{Ptr{Cchar}}unsafe_string
T *x[] output (PETSc-owned array)return x::Vector{T} if a size rule exists, else x::Ptr{T}Ptr{Ptr{T}}unsafe_wrap(Array, x_[], size; own = false) or the raw pointer
T x[] output (caller-allocated)return x::Vector{T} if a len rule exists or an argument ni exists, else input x::Vector{T}Ptr{T}x = Vector{T}(undef, len)
T *x[] input (Restore*)x::Vector{T}Ptr{Ptr{T}}x_ = Ref(pointer(x))
T *n input in Restore*n::TPtr{T}n_ = Ref{T}(n)
T **x output, not an arrayreturn x::Ptr{T}Ptr{Ptr{T}}raw pointer
function pointer (XFn *f, void (*f)(...))f::Ptr{Cvoid}Ptr{Cvoid}pass an @cfunction pointer
XFn **f outputreturn f::Ptr{Cvoid}Ptr{Ptr{Cvoid}}
void *ctxctx::Ptr{Cvoid}Ptr{Cvoid}
void **ctx outputreturn ctx::Ptr{Cvoid}Ptr{Ptr{Cvoid}}

Every wrapper is emitted as an untyped stub (which carries the docstring) plus one @for_petsc method per library. The stub's scalar types are loosened (PetscScalar -> Number, PetscInt -> Integer, ...) so it is never more specific than a generated method, and its body throws. A call whose arguments match no library method therefore fails loudly (the old stub returned nothing, which hid e.g. Float64 literals passed to a Float32 library in the tests).

Input handle arguments always take the abstract type (AbstractPetscVec), so VecPtr, MatShell and the typed DM hierarchy pass; return positions use the concrete type (PetscVec). test/lowlevel/wrapper_signatures.jl enforces this.

Rules (generator/rules/)

files.toml: one [[file]] per output file with classes = [...] (or standalone = true for functions not attached to a class), optional include = false to generate but not include. [exclude] lists functions never wrapped, with a reason. Functions containing _ are skipped.

types.toml: the C -> Julia replacements (fix_substring_replacements switches from the old substring replacement, which turned PetscPointFn into PetscPoCintFn, to word-boundary replacement), [[handles]] (C name, Julia struct, abstract type, C alias), [rename_args] (Julia keywords used as C argument names), [senum_overrides] (VecType = "Cstring"), and [predeclared] names the generator must not declare as opaque types.

ownership.toml: owned lists the Get functions that hand the caller a new reference. A handle returned by any other function with Get in its name is built with own = false, so destroy! on it does nothing (naming.md §18.2); every other returned handle has own = true. A function goes on the list only when its manual page or source is clear, since a wrong entry destroys an object twice.

args.toml (hand-maintained) and args_mined.toml (written by bootstrap_rules.jl, never edited; args.toml wins on conflicts): per argument, keyed [FunctionName.argname]:

keymeaning
direction = "in"/"out"/"inout"force the classification (inout: scalar passed in by pointer and returned)
nullable = trueinput also accepts a Ptr (so C_NULL can be passed)
byref = trueopaque handle passed as Union{X, Ref{X}}
size = "expr"length or dims to unsafe_wrap a PETSc-owned output array; may use other arguments/outputs
prelude = "code"line(s) emitted before the wrap, e.g. m, n = MatGetLocalSize(petsclib, A)
nullinit = trueinitialise the output Ref with C_NULL
len = "expr"length of a caller-allocated output Vector

Rules and overrides that name a function or argument absent from a new snapshot are reported by generator/apidiff.jl.

Overrides (generator/overrides/NAME.jl)

A file NAME.jl replaces the generated block for NAME verbatim (docstring, stub and @for_petsc method). Its first line records the C signature it was written against, so a changed signature can be flagged. Files whose name is not a function in the snapshot are collected into extra_wrappers.jl (macros such as PETSC_VIEWER_STDOUT_WORLD, hand-written helpers such as DMProjectFunction, and the PetscSFBcast*/PetscSFReduce*/PetscSFFetchAndOp* communication routines, which getAPI.py rejects because they take an MPI_Datatype). Use an override only when no rule can express the wrapper (multi-dimensional PetscArray views, MatGetRowIJ, PetscOptionsGetString, ...).

Tools

scriptpurpose
generate.jlregenerate src/autowrapped
apidiff.jl OLD.json NEW.jsonnew / removed / signature-changed functions between two snapshots, and rules or overrides that no longer match
golden_diff.jl A B [--categorize] [--show NAME]function-level comparison of two autowrapped directories
check_callers.jl [DIR]every LibPETSc.X(...) call in src/, ext/, test/, examples/ whose argument count does not match the generated stub (first thing to run after a regeneration that changed conventions)
bootstrap_rules.jl GOLDENone-off: mine rules/args_mined.toml from a hand-edited directory
make_overrides.jl GOLDEN NAME...copy hand-written blocks into overrides/
check_symbols.jl (run with --project=.)wrapped functions that are not symbols of the PETSc_jll library: header inlines and macros belong in [exclude], optional-package functions are fine

.github/workflows/wrappers.yml regenerates from the PETSc tarball on every change to src/autowrapped or wrapping/generator and fails if the committed files differ. test/lowlevel/wrapper_quality.jl infers the return type of every generated method (must be concrete) and checks representatives of each argument kind with @inferred and @allocated.

Checking a regeneration

golden_diff.jl compares two autowrapped directories block by block, independent of the order of functions in a file:

julia --project=wrapping/generator wrapping/generator/golden_diff.jl OLD_DIR NEW_DIR --categorize
julia --project=wrapping/generator wrapping/generator/golden_diff.jl OLD_DIR NEW_DIR --show VecGetArray

--categorize buckets every differing wrapper (docs-only, voidptr-fix, writeback-fix, ...) and writes the lists to cat_*.txt in $GOLDEN_DIFF_OUT (default: temp dir). The categories that represent deliberate differences from the old hand-edited files are listed in DEVIATIONS.md.

After regenerating, always run

grep -rn "isa Ref ?" src/autowrapped/                    # must be empty (Ptr{T} <: Ref{T})
grep -rn "VecGetLocalSize(petsclib, x)" src/autowrapped/ # placeholder sizes: must be empty
julia --project=. -e 'using Pkg; Pkg.test()'             # includes test/lowlevel/wrapper_signatures.jl

Moving to a new PETSc release (what happened for 3.24 -> 3.25)

  1. Fetch the tarball, dump the snapshot, run apidiff.jl OLD NEW and read the report.
  2. Generate into a scratch directory; the generator warns about classes not assigned to any file (3.25 added PetscDA and TaoTerm: add them to rules/files.toml) and about typedefs it skips (a typedef whose value is unknown, e.g. PetscComplex = __complex128, is declared as an opaque type instead; typedefs the prologue already defines, PetscInt, PetscScalar, ... are excluded automatically).
  3. New C idioms may need a classifier rule: 3.25 introduced typedef void *PetscCtx / PetscCtxRt, which the classifier treats as void *.
  4. getAPI.py mis-parses a few declarations (unsigned char R[] -> type unsigned, name char; int (*cmp)(...) -> name int cmp); the loader sanitises names and types.toml maps unsigned to Cuchar. Check the generator's "type names used but not defined" warning.
  5. Load the package (using PETSc), run check_callers.jl, then the test suite with the matching PETSc_jll (bump its compat in Project.toml).

Things that bite

  • The manual page of a PETSc function sometimes ends with */ instead of @*/; the indexer stops a block at */ or at the next /*@ so the following page is not swallowed.
  • The first docstring line is the text after the first - of Name - description; a description containing - is truncated (Normalizes a vector by its 2 for 2-norm). This reproduces the old output; fix it in docs.jl (_finish_block) if you want.
  • getAPI.py gives void (*f0)(...) arguments the type name void(f0 and an empty name; the classifier turns these into f0::Ptr{Cvoid}.
  • C++ scoped names (moab::Range, std::size_t) become moab_Range, Csize_t.
  • Argument names that are Julia keywords (begin, end, function, global, local, ...) get a trailing underscore or a short alias (see [rename_args]).
  • @for_petsc bodies get $PetscScalar, $PetscReal, $PetscInt, $PetscComplex by plain substring replacement (dispatch_types), exactly like the old generator.
  • struct_wrappers.jl is hand-maintained (generator/structs.jl): field order must match the C struct. Check api/petsc-X.Y.Z.json (structs) when moving to a new release.
  • PetscBool (generator/petscbool.jl) is an 8-bit primitive because PETSc >= 3.24 defines typedef bool PetscBool; it was a 4-byte enum before. Check typedefs.PetscBool in the snapshot when moving to a new release: the width decides outputs, arrays and struct layouts.
  • The prologue (generator/prologue.jl) is the only copy of the handle structs. Never declare a PETSc handle there as an empty mutable struct X end: Ref{X}() is then an undefined reference and a ccall passes a Julia object pointer. Leave it to opaque_types.jl (const X = Ptr{_n_X}).
  • getAPI.py lists some static inline header functions (PetscStrcmp, PetscTime, VecSetValue, MatSetValue) and macros (PetscOptionsBegin): they have no symbol in libpetsc, so they are in [exclude]. Run check_symbols.jl after moving to a new release.
  • getAPI.py drops every function with an MPI_Datatype argument (rejects list), so the PetscSF communication routines are overrides.
  • PETSc's manual pages are not always right about directions: PetscObjectGetName lists its output under Input Parameters, PCMGSetLevels' optional comms is a MPI_Comm * that would be taken as an output. Both are fixed in args.toml.

History

The wrappers were first produced (PETSc 3.23/3.24) by a PythonCall-based script, wrapping/generatejuliabindings.jl, whose output was then fixed by hand in src/autowrapped/ over several PRs (#254, #257, #258, #259, #261, #263). Those fixes were lost on every rerun, which is why the generator was rewritten in September 2026 (see DEVIATIONS.md for what changed in the output; the analysis and plan of the rewrite are in the git history as REWRITE_PLAN.md). The old script, its helper files and the REGENERATING.md notes that accompanied PR #263 were removed afterwards; they remain in the git history before commit 0d58b5f.