API reference
This reference lists the public APIs of the wrapper generator and its ABI carrier package. For task-oriented use, start with Your first wrapper, Declaring an API with @api, or Building and distributing a library.
JuliaLibWrapping
JuliaLibWrapping.standard_build — Function
standard_build(dir = pwd(); libname, kwargs...)Run build_library with defaults for the conventional single-library layout:
dir/
├── Project.toml # entry project (runtime deps only)
├── src/
│ └── <libname>.jl # @ccallable entrypoints
└── out/ # generated artifactsEmits both a C header and a Python ctypes package (<libname>_py), bundled for distribution. Equivalent to:
build_library(joinpath(dir, "src", libname*".jl"),
[CTarget(joinpath(dir, "out"), libname),
PythonTarget(joinpath(dir, "out"), libname*"_py", libname;
bundle_subdir = "bundle", version = "0.0.0")];
project = dir, libname, libdir = joinpath(dir, "out"),
bundle = true, kwargs...)The kwargs out, entry, python_package, project, bundle, and version override the defaults above; anything else is forwarded to build_library (e.g. verbose, trim, privatize). project defaults to dir, but can be pointed at a separate location when the on-disk source layout and the entry Project.toml live in different directories. version sets the version in the generated Python package's pyproject.toml (see PythonTarget). For layouts outside this convention, call build_library directly.
JuliaLibWrapping.build_library — Function
build_library(entry, targets;
project=dirname(entry), libname, libdir=pwd(),
abi_path=joinpath(libdir, libname*".abi.json"),
trim=:safe, compile_ccallable=true,
backend=:auto, verbose=false,
bundle=false, bundle_dir=joinpath(libdir, libname*"-bundle"),
privatize=bundle, cpu_target=nothing)Run the full juliac → ABI JSON → wrapper pipeline in one call.
entry is the path to the Julia source file (or package directory) that juliac will compile; targets is a vector of AbstractTargets that will each receive a write_wrapper call once the ABI JSON is available.
Returns a NamedTuple (library, abi_path, abi_info, target_outputs, backend, bundle_dir, metadata_path). bundle_dir is the path to the produced bundle tree when bundle = true, and nothing otherwise. metadata_path is the path to the <libname>.jlw.json API metadata sidecar (see the "API metadata sidecar" section below), or nothing when the entry project has no JLWInterop dependency or defines no @api functions.
API metadata sidecar
When project's Project.toml lists JLWInterop in [deps], build_library runs a subprocess, before the juliac step, that includes entry and calls JLWInterop.write_metadata to dump every @api-annotated function's name, positional and keyword arguments, enum-typed argument/return types, and docstring to <libname>.jlw.json in libdir. Once juliac has run, check_metadata_consistency validates the sidecar against the ABI JSON: an unknown symbol, an argument mismatch, or an enum reference that does not resolve is a build error. Targets can use the validated metadata's public names, keyword arguments, enum types, and docstrings. The current PythonTarget receives them as write_wrapper's api_metadata and api_enums keywords. An entry file that defines no @api functions produces no sidecar, and every target emits as it would without one.
Example
using JuliaLibWrapping, JuliaC
out = mktempdir()
result = build_library(
joinpath(@__DIR__, "src/mylib.jl"),
[CTarget(out, "mylib"), PythonTarget(out, "mylib_py", "mylib")];
project = @__DIR__,
libname = "mylib",
libdir = out,
)Extended help
Backend
build_library drives JuliaC.jl (a weak dependency); load it with using JuliaC before calling this function. backend = :auto (the default) and backend = :juliac are synonyms; the keyword is retained so additional backends can be added without changing the calling interface.
Relative [sources] paths
build_library supports relative [sources] paths and relative paths for developed dependencies in a manifest. Because juliac relocates the project, compilation uses a temporary copy with those paths made absolute. The original project is unchanged. Paths must refer to existing files or directories.
Bundling
A juliac-produced .so depends on libjulia, a sysimage, stdlibs, and artifacts — none of which a pip install-ing Python user has on their machine. Pass bundle = true to also produce a self-contained directory tree (the juliac --bundle layout) and copy it into every PythonTarget's package. The Python loader generated for those targets searches the bundle first, so the embedded RUNPATH resolves libjulia from inside the wheel at import time.
bundle = true requires the :juliac backend and that each PythonTarget declare a bundle_subdir (e.g. PythonTarget(out, "mylib_py", "mylib"; bundle_subdir = "bundle")). Targets that are not Python (e.g. CTarget) are unaffected — C consumers manage their own linkage.
privatize salts the bundled libjulia and libjulia-internal with a distinct SONAME prefix, so the loader cannot satisfy this library's runtime dependency from another loaded copy. It defaults to bundle. See the manual section on multiple wrapped libraries in one process.
Privatization applies to the bundle, so privatize = true with bundle = false is an error rather than a silent no-op. Pass privatize = false alongside bundle = true to opt out.
CPU target
cpu_target sets the multi-microarchitecture target for the compiled library, using the same syntax as the --cpu-target julia flag or the JULIA_CPU_TARGET environment variable (e.g. "generic;sandybridge,-xsaveopt,clone_all"). The default, nothing, defers to JULIA_CPU_TARGET if it is set in the environment, or otherwise to the host CPU.
JuliaLibWrapping.write_wrapper — Function
write_wrapper(target::AbstractTarget, abi_info::ABIInfo)Emit wrapper source files for abi_info into the location described by target. The methods that ship are dispatched on CTarget (one .h file) and PythonTarget (a Python ctypes package directory). Add a method for a new AbstractTarget subtype to support another output language.
JuliaLibWrapping.AbstractTarget — Type
AbstractTargetSupertype of wrapper-emission targets. Each concrete subtype is a configuration struct describing where and how to emit one output language's bindings; a corresponding write_wrapper method consumes that configuration plus an ABIInfo and writes the files.
Ships today: CTarget for a C header, PythonTarget for a Python ctypes package. New languages are added by defining a subtype and a write_wrapper method for it.
JuliaLibWrapping.CTarget — Type
CTarget(dir, headerbase)Output configuration for a C header. write_wrapper writes a single file joinpath(dir, headerbase * ".h") containing typedefs for every struct in the ABI and extern declarations for every entrypoint.
Only the primitive Julia types listed in the emitter's ctypes table have a direct C mapping; an ABI referencing any other primitive raises an error. Pointer types are emitted inline as T* rather than as separate typedefs. Non-C-safe identifiers are scrubbed by sanitize_for_c and disambiguated with a numeric suffix on collision.
JuliaLibWrapping.PythonTarget — Type
PythonTarget(dir, package_name, library_basename;
bundle_subdir = nothing, version = "0.0.0",
privatized = false)Output configuration for a Python ctypes-based wrapper package. dir is the directory into which the package will be written; a sub-directory named package_name is created and is the importable Python module. library_basename is the shared library's basename without an OS-specific suffix (e.g. "libsimple", which will be loaded from libsimple.so / libsimple.dylib / libsimple.dll depending on the host).
When bundle_subdir is a string (e.g. "bundle"), the emitter assumes the shared library and its juliac runtime closure (libjulia, sysimage, stdlibs, artifacts) will be laid out under that subdirectory of the Python package in the standard --bundle shape (<bundle_subdir>/lib/<lib>, <bundle_subdir>/lib/julia/, <bundle_subdir>/artifacts/). The generated loader looks for the library inside the bundle first, the generated pyproject.toml widens package-data to include the bundle tree, and build_library with bundle = true will copy the bundle there. The default nothing preserves the flat single-.so-next-to-the-package layout and is the right choice for callers placing the library by hand.
version sets the version field in the generated pyproject.toml. It must be PEP 440-compatible; a Julia Major.Minor.Patch version string is valid.
privatized records whether the bundle carries a salted libjulia. A package without one warns if another wrapped package is already loaded. build_library sets this option; pass it directly only when using write_wrapper for a bundle built elsewhere.
JuliaLibWrapping.ABIInfo — Type
ABIInfoFields
typeinfo: type descriptors in declaration order.forward_declared: type IDs requiring C forward declarations.entrypoints: exported functions.
JuliaLibWrapping.read_abi_info — Function
abi_info = read_abi_info(filename::AbstractString)
abi_info = read_abi_info(io::IO)Read and parse a juliac ABI-info JSON file, returning an ABIInfo. The first form is equivalent to parse_abi_info(JSON.parsefile(filename)); the second reads the document from a stream.
JuliaLibWrapping.parse_abi_info — Function
abi_info = parse_abi_info(parsed::AbstractDict)Build an ABIInfo from a parsed juliac ABI-info JSON document. parsed is the dictionary returned by JSON.parsefile (or JSON.parse) on such a file.
See read_abi_info for the file-based convenience.
JLWInterop
JLWInterop.JLWInterop — Module
JLWInteropDependency-free ABI types for JuliaLibWrapping-generated libraries, plus @api, the annotation macro that generates a @ccallable wrapper (argument/return conversion, JLWResult/JLWStatus error reporting, and a build-host metadata registry) from an ordinary Julia function.
JLWInterop.@api — Macro
@api [docstring] name(a::T1, ...; k::K = default, ...)::RetDeclare one call signature of name as a JuliaLibWrapping API entry point. name must already be callable with those types — defined in this module, or brought in from the package this binding layer wraps. The macro defines nothing itself: it generates a Base.@ccallable C-ABI wrapper (named by the private _api_symbol helper) that converts arguments and return value through carrier_type/carrier_return_type/to_carrier/from_carrier and reports errors via JLWResult/JLWStatus, and records an ApiEntry in the declaring module's registry (see the private _REGISTRY_NAME binding).
The declared types are the boundary contract, not a method signature: name may accept more than this, and foreign callers get exactly this. Declaring a signature name cannot satisfy is caught when the library is compiled, as a missing method.
A body is rejected. Writing one would define a function, and in a binding layer that imports the wrapped function by name it would silently add a method to it instead — which recurses when the declared signature is the more specific one.
Every argument and the return type must resolve (via Core.eval in the declaring module) to a type with a carrier mapping. Types are resolved at macro-expansion time, so any alias used in the signature must already be defined. Keyword defaults must be literals (Int, Float, Bool, String, or nothing) of the keyword's declared type: k::Float64 = 2 is rejected, k::Float64 = 2.0 is accepted. An enum keyword default must be a bare member name or dotted member path, resolved in the declaring module.
Keyword arguments are positional in the C ABI. They follow the positional arguments, in declaration order, and every one is passed on every call: a default is applied by the calling side, which reads it from the metadata sidecar, not by the wrapper. The wrapper therefore has one arity, and a keyword's default is a property of the binding rather than of the entry point.
JLWInterop.@export_release_entrypoints — Macro
@export_release_entrypointsAt module top level, emit the release functions required by owning carrier returns. jlw_free frees one allocation; jlw_free_strings frees an array of CStrings and their buffers.
JLWInterop.JLWStatus — Type
JLWStatusABI status value. code == 0 means success; message is a fixed-size, NUL-terminated UTF-8 buffer.
JLWInterop.JLWResult — Type
JLWResult{C}Return carrier for a generated API wrapper: a JLWStatus plus a value. A zero status.code means value is meaningful; on any nonzero code value is zero-filled, and its pointers are null.
JLWInterop.jlw_ok — Function
jlw_ok() -> JLWStatusReturn a success status: code 0 with an empty message buffer.
jlw_ok(value::C) -> JLWResult{C}Wrap a successful value: status is jlw_ok() and value is value.
JLWInterop.jlw_error — Function
jlw_error(code::Integer, msg::AbstractString) -> JLWStatusReturn an allocation-free error status. code is converted to Int32; msg is truncated as needed and NUL-terminated.
jlw_error(code::Integer, msg::AbstractString, ::Type{C}) -> JLWResult{C}Wrap a failure: status is jlw_error(code, msg) and value is a zero-filled C (via the private JLWInterop._zero_carrier helper) with a null pointer.
JLWInterop.JLW_MESSAGE_BYTES — Constant
JLW_MESSAGE_BYTESSize of the inline JLWStatus message buffer. One byte is reserved for the terminating NUL.
JLWInterop.CArray — Type
CArray{owned,T,N}Column-major N-D buffer for @ccallable boundaries. data points to prod(dims) contiguous elements of T.
Ownership contract
owned is :owned or :borrowed, so whether a value must be released is a property of its type.
CArray{:owned,T,N} holds Julia-allocated storage, produced by CArray{:owned}(::AbstractArray). The consumer releases data exactly once, with Libc.free or the jlw_free entrypoint emitted by @export_release_entrypoints.
CArray{:borrowed,T,N} wraps memory the caller owns and keeps alive; the consumer never releases it, and must make the storage writable before mutating it. Pointer constructors build carriers of either ownership.
Example
using JLWInterop
Base.@ccallable function sum_values(a::CArray{:borrowed,Float64,2})::Float64
return sum(a)
endExtended help
T should be an isbits type. CArray <: DenseArray and supports linear indexing, the strided-array interface, and conversion to Ptr{T}. The aliases CVector and CMatrix cover one and two dimensions.
GC.@preserve on a carrier protects nothing: the buffer is not garbage-collected memory. An owned buffer is valid until it is released; a borrowed one is valid for as long as its true owner keeps it so.
Every construction path names an ownership: there is no defaulting constructor, and any parameter other than :owned or :borrowed is rejected.
Binding targets can map this layout to native N-D array types without changing the ABI. They must preserve column-major dimension and stride semantics. The two ownerships are two distinct types, so a target reads "release this" or "do not release this" off the signature alone.
JLWInterop.CVector — Type
CVector{owned,T}Alias for CArray{owned,T,1}. See CArray.
JLWInterop.CMatrix — Type
CMatrix{owned,T}Alias for CArray{owned,T,2}, laid out in column-major order. See CArray.
JLWInterop.CString — Type
CString{owned}Length-prefixed UTF-8 string descriptor for @ccallable boundaries. It contains length bytes at data and permits embedded NUL bytes.
Ownership contract
owned is :owned or :borrowed.
CString{:owned}(::AbstractString) allocates a copy. The consumer releases data once with Libc.free or the jlw_free entrypoint emitted by @export_release_entrypoints.
CString{:borrowed} wraps a buffer the caller owns and keeps alive; the consumer never releases it.
Example
using JLWInterop
Base.@ccallable function greeting_length(s::CString{:borrowed})::Int32
return Int32(length(s))
end
s = CString{:owned}("héllo")
String(s) == "héllo"
Libc.free(s.data)Extended help
Unlike Base.Cstring, CString is length-prefixed rather than NUL-terminated.
Use it instead of a String, which is not C-ABI compatible, or a Cstring, which requires NUL termination and forbids embedded NULs.
Every construction path names an ownership: there is no defaulting constructor, and any parameter other than :owned or :borrowed is rejected.
CString{owned} <: AbstractString. Use String(s) to copy its bytes into a Julia String.
JLWInterop.CStrArray — Type
CStrArray{owned}Array of length-prefixed UTF-8 CStrings for C ABI boundaries.
Ownership contract
owned is :owned or :borrowed, so whether a value must be released is a property of its type rather than of the value.
CStrArray{:owned} holds Julia-allocated storage, produced by CStrArray{:owned}(::AbstractVector{<:AbstractString}). The consumer releases it exactly once with _free_strings or the jlw_free_strings entrypoint emitted by @export_release_entrypoints.
CStrArray{:borrowed} wraps memory the caller owns and keeps alive; the consumer never releases it. Converting to Vector{String} copies without freeing the source.
Elements share the container's ownership — data points to CString{owned}s — and releasing an owning CStrArray releases every element's buffer too.
There is no default ownership or constructor that borrows a Vector{String}; borrowed carriers arrive across the ABI.
CStrArray{owned} <: AbstractVector{CString{owned}} and is read-only. Collected elements still alias the carrier's buffers, which must be released only through the original carrier.
Example
using JLWInterop
a = CStrArray{:owned}(["hello", "world"])
Vector{String}(a) == ["hello", "world"]
String.(a) == ["hello", "world"]
JLWInterop._free_strings(a.data, a.length)JLWInterop.CDict — Type
CDict{owned,V}String-keyed dictionary for C ABI boundaries. Keys are length-prefixed CStrings and values are a parallel array of a type in CDICT_VALUE_TYPES.
Ownership contract
owned is :owned or :borrowed, so whether a value must be released is a property of its type.
CDict{:owned,V} holds Julia-allocated storage, produced by CDict{:owned}(::AbstractDict{<:AbstractString,V}). The consumer releases keys with _free_strings and values with Libc.free, or uses the corresponding entrypoints emitted by @export_release_entrypoints, exactly once.
CDict{:borrowed,V} wraps memory the caller owns and keeps alive; the consumer never releases it. Converting to a Dict copies without freeing the source.
Keys share the dictionary's ownership — keys points to CString{owned}s — and releasing them releases every key's buffer too.
There is no default ownership or constructor that borrows a Dict; borrowed carriers arrive across the ABI.
Example
using JLWInterop
c = CDict{:owned}(Dict("a" => 1.5, "b" => -2.0))
Dict{String,Float64}(c) == Dict("a" => 1.5, "b" => -2.0)
JLWInterop._free_strings(c.keys, c.length)
Libc.free(c.values)JLWInterop.COpt — Type
COpt{T}C-ABI representation of Union{T,Nothing}. has_value is 1 when the inline value is present and 0 when it is absent. Absent values are zero-filled.
Construct with COpt(x) (present) or COpt{T}(nothing) (absent, zero-filled); read back with Base.get(opt, default).
Example
using JLWInterop
get(COpt(3.5), nothing) === 3.5
isnothing(get(COpt{Float64}(nothing), nothing))JLWInterop.CDICT_VALUE_TYPES — Constant
CDICT_VALUE_TYPESSupported value types for CDict. The closed list permits concrete, trim-safe conversion methods; other value types throw MethodError.
JLWInterop.carrier_type — Function
carrier_type(::Type{T}) -> Union{Type, Nothing}The C-ABI argument carrier for T, or nothing when no mapping exists. Storage-backed argument carriers are borrowed. Dictionary values, optional payloads, and array elements must be concrete scalar bits types.
Array{T,N} and StridedArray{T,N} arguments use the same borrowed CArray carrier. An Array argument receives an unsafe_wrapped view; resizing it detaches it from the carrier's buffer. A StridedArray argument receives the carrier directly.
A Ptr{T} is its own carrier and crosses unconverted: neither a length nor an owner travels with it. An argument addresses memory the caller owns. A return must address memory that outlives the call and that the caller can already reach — a buffer it passed in, or storage the library holds onto. A pointer into a fresh Julia allocation dangles as soon as the collector runs; one into Libc.malloc memory leaks, because nothing on the far side knows to free it.
A concrete Base.Enum{B} subtype is carried as B. Incoming values are validated against the enum's members. Optional enums have no mapping.
The mapping is open. A type outside this table becomes an @api type once its own module adds methods for it to carrier_type, to_carrier and from_carrier; an isbits struct can be its own carrier, with all three methods the identity.
JLWInterop.carrier_return_type — Function
carrier_return_type(::Type{T}) -> Union{Type, Nothing}The C-ABI return carrier for T. Storage-backed return carriers are owned; other mappings match carrier_type.
JLWInterop.to_carrier — Function
to_carrier(x) -> carrier valueConvert a native value to its C-ABI carrier, per carrier_type.
JLWInterop.to_carrier_as — Function
to_carrier_as(::Type{T}, x) -> carrierConvert x to T, then to its carrier.
JLWInterop.from_carrier — Function
from_carrier(::Type{T}, c) -> TConvert a C-ABI carrier back to the native T.
JLWInterop.ApiEntry — Type
ApiEntryMetadata for an @api declaration: its Julia name, C symbol, positional args, keyword kwargs, return type ret, and docstring.
A keyword's default is the literal value itself, already of type type; has_default is false for a required keyword, and its default is then nothing.
JLWInterop.api_entries — Function
api_entries(root::Module = Main) -> Vector{ApiEntry}Every @api entry declared in root or in a module nested under it, in declaration order per module. Base and Core are not searched.
JLWInterop.write_metadata — Function
write_metadata(path::AbstractString, root::Module = Main)Write the JSON metadata sidecar for every @api declaration in root or a module nested under it (see api_entries) to path.
Files without enums use version 1:
{"jlw_metadata_version": 1, "exports": {symbol: {"name", "args", "kwargs", "doc"}}}Files with enums use version 2 and add an enums table:
{"jlw_metadata_version": 2,
"enums": {name: {"basetype", "members": [{"name", "value"}, ...]}},
"exports": {symbol: {"name", "args", "kwargs", "arg_enums"?, "return_enum"?, "doc"}}}exports entries are sorted by symbol for stable output. args is the positional argument names, in declaration order. kwargs is a list of {"name": ...} for a required keyword-only argument (see ApiEntry) or {"name": ..., "default": ...} for one with a default. Enum defaults are stored by member name; other defaults retain their JSON type. ABI types remain in the JSON produced by juliac.
Each enums entry records the base integer type and the members in declaration order. Enum names must be unique across the exported API.
arg_enums maps argument names to enum names. return_enum names an enum return type. Empty annotations are omitted.
The JSON is written by hand so that JLWInterop needs no JSON dependency.