Generated Python bindings
PythonTarget emits a Python package with two generated layers and an author-controlled public layer:
mylib_py/
├── __init__.py
├── _lowlevel.py
├── _generated.py
└── _facade.pyApplications normally import from mylib_py. Direct ABI access remains available as mylib_py._lowlevel.
Regenerated low-level bindings
_lowlevel.py is rewritten on every build. It contains:
ctypes.Structuredefinitions for ABI structs and carriers;ctypessignatures for exported entrypoints;- generated enums from API metadata;
- carrier conversion and release helpers;
JLWError; and- shared-library discovery and loading.
Every structure is followed by an import-time layout check against the sizes and offsets recorded by juliac. A mismatch fails at import rather than silently interpreting memory with the wrong layout.
Do not edit this file: regeneration replaces it.
Generated façade
_generated.py is rewritten on every build. It exposes idiomatic functions for recognized signatures and leaves a TODO: hand-wrap re-export when target-specific policy is required. Do not edit this file: regeneration replaces it.
With @api metadata, generated functions preserve the declared public name, positional/keyword split, keyword defaults, enums, and docstring. Carrier arguments and results become the Python values listed in Supported Julia types. A non-zero status raises JLWError, whose .code and .message attributes retain the library error.
Hand-written @ccallable entrypoints have no API sidecar declaration. The generator still recognizes common carrier shapes and status returns, but it cannot recover Julia-level names, keyword arguments, defaults, or docstrings. Unrecognized structs, raw ownership transfers, and ambiguous policies are deliberately left to a human.
Array coercion
By default a generated wrapper hands an array argument to the carrier's from_numpy, which raises ValueError when the dtype or memory order differs from the library's. With PythonTarget(...; coerce_arrays = true) the wrapper first applies np.asfortranarray(x, dtype=...) to an array of two or more dimensions and np.ascontiguousarray(x, dtype=...) to a vector:
def trace(m):
_m = CMatrix_borrowed_Float64.from_numpy(np.asfortranarray(m, dtype="float64"))
return _lowlevel.trace(_m)Input that does not already match is copied without notice, so the option is off by default.
Author-editable façade
_facade.py is created only if it does not exist, and rebuilding never overwrites it. The starter imports everything _generated.py defines and copies its __all__, so a package with no hand-written layer exposes the generated API unchanged. Keep the file under version control.
A _facade.py written by JuliaLibWrapping 0.2 or earlier contains a full copy of the wrappers and does not import _generated. It keeps working, but a rebuild no longer updates its wrappers. To move to the split layout, delete it, rebuild to get the starter, and add the hand-written parts back.
To change the public API:
- to add a function, define it in
_facade.pyand append its name to__all__; - to override a generated function under its public name, define it after the star import; the generated one stays reachable as
_generated.<name>; - to hide a generated name, remove it from
__all__.
An override that builds on the generated wrapper reaches it through the module:
import numpy as np
from . import _generated
from ._generated import * # noqa: F401,F403
__all__ = list(_generated.__all__)
def gramcover(A, tol=1e-8):
"""Cover the columns of `A`, dropping near-duplicates."""
return _generated.gramcover(np.asarray(A, dtype="float64"), tol)Package exports
__init__.py is regenerated and re-exports whatever _facade.__all__ lists. Functions added to _facade.py therefore become package-level imports, while the mechanical layer stays explicitly accessible as mylib_py._lowlevel.
Error conversion
An @api entrypoint catches exceptions and returns a JLWResult or direct JLWStatus; the façade raises JLWError on failure and returns the converted value on success:
from mylib_py import scale, JLWError
try:
scale(bad_input)
except JLWError as err:
print(err.code, err.message)For a hand-written entrypoint, the same conversion applies when the return is a direct JLWStatus or a struct containing one. On success an unrecognized result struct remains a low-level ctypes value.
Library loading
For a bundled package the loader searches the configured bundle directory first, then falls back to a shared library beside the package. A privatized bundle records that it can safely coexist with other wrapped libraries. A non-privatized package warns when another JuliaLibWrapping package is already loaded in the process.
See Bundling for distribution and Multiple wrapped libraries in one process for the runtime implications.