JLWInterop
JLWInterop.JLWInterop — Module
JLWInteropCanonical types for JuliaLibWrapping cross-ABI conventions. Provides JLWStatus for in-band error reporting and CArray (with CVector and CMatrix aliases) for N-D numeric buffers; both are designed to cross a @ccallable boundary.
A library author opts in by using these types in any @ccallable-returned or @ccallable-accepted struct; the JuliaLibWrapping Python emitter then recognizes them by struct name + shape and emits idiomatic code — raising a native Python exception on a non-zero JLWStatus.code, and exposing from_numpy / as_numpy helpers on each recognized CArray.
This package deliberately has no dependencies so it stays juliac --trim-friendly: it is intended to be a build-time and runtime dependency of compiled libraries.
Ownership and layout discipline
Every type in this package holds a raw pointer and does not own the underlying storage. The caller that allocated the buffer is responsible for keeping it alive for the duration of any call that sees it, and for freeing it afterward. In exchange, every type is isbits (when the element type is), is juliac --trim-friendly, and crosses a @ccallable boundary without heap allocation.
The JuliaLibWrapping Python emitter recognizes these types structurally — by struct name plus field shape — so an author who copy-pastes a compatible definition into their own library gets the same wrapper behavior. Depending on JLWInterop is the way to keep the definitions from drifting across libraries.
JLWStatus — in-band error reporting
A library that needs to surface errors to its caller (rather than abort the process) returns either a JLWStatus directly, or a struct that contains a JLWStatus field. code == 0 is success; any non-zero value is an error code the library defines. message is a fixed-size UTF-8 buffer, null-terminated within the buffer.
The buffer is inline and fixed-size (JLW_MESSAGE_BYTES bytes) on purpose: a Cstring or Ptr{UInt8} would force a decision about who allocates and frees the message, which has no good answer under juliac --trim. The price is a bounded message length; the benefit is that constructing a status performs no heap allocation.
Construct values with the helpers:
using JLWInterop
Base.@ccallable function safe_sqrt(x::Float64)::JLWStatus
x < 0 && return jlw_error(1, "negative input")
return jlw_ok()
endSee Error handling across the ABI for the full library-author-and-Python-caller round trip, including how the emitter raises JLWError on the Python side.
JLWInterop.JLWStatus — Type
JLWStatusABI-stable status struct. code == 0 means success; any non-zero value is an error code chosen by the library. message is a UTF-8, null-terminated buffer of fixed length (JLW_MESSAGE_BYTES bytes). The fixed buffer avoids the ownership/lifetime question that Cstring/Ptr{UInt8} would raise under --trim; payload size is bounded but no allocation is required to construct one.
JLWInterop.jlw_ok — Function
jlw_ok() -> JLWStatusReturn a success status: code 0 with an empty message buffer.
JLWInterop.jlw_error — Function
jlw_error(code::Integer, msg::AbstractString) -> JLWStatusReturn an error status with the given non-zero code and msg. msg is copied into the fixed-size buffer, truncated to JLW_MESSAGE_BYTES - 1 bytes if necessary, and always null-terminated. code is converted to Int32.
Constructing this status performs no heap allocation.
JLWInterop.JLW_MESSAGE_BYTES — Constant
JLW_MESSAGE_BYTESThe fixed size, in bytes, of the inline message buffer inside JLWStatus. Sets the maximum message length an error can carry across the ABI; longer strings passed to jlw_error are truncated to JLW_MESSAGE_BYTES - 1 bytes plus a terminating NUL.
CArray{T,N} — N-D numeric buffer (column-major)
CArray{T,N} is (dims::NTuple{N,Int32}, data::Ptr{T}), laid out in column-major order — the same convention as Array{T,N} and Fortran, not C. The caller owns the storage; CArray borrows. For primitive numeric T, the Python emitter generates from_numpy / as_numpy helpers so a Python caller can pass a numpy.ndarray directly without copying.
The 1-D and 2-D specializations have familiar aliases:
const CVector{T} = CArray{T,1}
const CMatrix{T} = CArray{T,2}mirroring Julia's own Vector{T} = Array{T,1} / Matrix{T} = Array{T,2}. You can use either the alias or the underlying CArray{T,N} form; they are the same type.
The column-major choice has a sharp consequence on the Python side for N ≥ 2: the generated from_numpy helper requires a Fortran-contiguous array and rejects a default row-major ndarray. Silently treating a row-major buffer as column-major would transpose without warning, which is a footgun no caller asks for. Python callers wrapping a default numpy array must .copy(order='F') (or np.asfortranarray(arr)) first.
CArray{T,N} <: AbstractArray{T,N} with IndexLinear() style, so the type participates in iteration, broadcasting, sum, views, and any function that accepts an AbstractArray{T,N} — at zero allocation. setindex! is defined unconditionally; only call it on storage you know to be writable.
JLWInterop.CArray — Type
CArray{T,N}ABI-stable N-D buffer descriptor for crossing a @ccallable boundary: prod(dims) contiguous elements of type T starting at data, laid out in column-major order (the same convention as Julia's Array{T,N} and Fortran). T should be an isbits type, in which case CArray{T,N} is itself isbits and allocation-free to construct, keeping the type juliac --trim-friendly. The dims field is NTuple{N,Int32}, matching the rest of the JLWInterop ABI vocabulary.
The 1-D and 2-D specializations have familiar aliases:
const CVector{T} = CArray{T,1}
const CMatrix{T} = CArray{T,2}CArray{T,N} holds a raw pointer; it does not own the underlying buffer. Whoever allocated the storage (the caller passing the buffer in, or the library that returned it) remains responsible for keeping it alive for the entire time any CArray referring to it is in use, and for freeing it afterward. CArray performs no allocation, no copy, and no finalization.
Use it to expose a numeric buffer at a @ccallable boundary instead of a Vector/Matrix/Array (which are not C-ABI compatible). The JuliaLibWrapping Python emitter recognizes CArray{T,N} for primitive numeric T and generates from_numpy / as_numpy helpers on the corresponding ctypes.Structure. For N ≥ 2 the storage is column-major, so from_numpy requires a Fortran-contiguous numpy.ndarray and rejects the default row-major layout: callers passing a default numpy array must write np.asfortranarray(arr) (or equivalent) first. This is deliberate — silently treating a row-major array as column-major would reinterpret the data without warning.
CArray{T,N} <: AbstractArray{T,N} and implements size, bounds-checked getindex, and setindex! via unsafe_load / unsafe_store! on data, with IndexLinear() style over the column-major storage. CArray participates in iteration, broadcasting, sum, views, LinearAlgebra routines, and any function that accepts an AbstractArray{T,N} — at zero allocation. setindex! is defined unconditionally, so callers must only invoke it on buffers they know to be writable.
Example
using JLWInterop
Base.@ccallable function sum_cvector(v::CVector{Float64})::Float64
return sum(v)
end
Base.@ccallable function trace_cmatrix(m::CMatrix{Float64})::Float64
n = min(size(m, 1), size(m, 2))
s = 0.0
@inbounds for i in 1:n
s += m[i, i]
end
return s
end
Base.@ccallable function sum3d(a::CArray{Float64,3})::Float64
return sum(a)
endThe caller (in Julia, Python, or C) is responsible for ensuring a.data points to at least prod(a.dims) valid T slots, in column-major order, for the duration of the call, and that the slots are writable when setindex! is used.
JLWInterop.CVector — Type
CVector{T}Alias for CArray{T,1}. See CArray.
JLWInterop.CMatrix — Type
CMatrix{T}Alias for CArray{T,2}, laid out in column-major order. See CArray.
CString — length-prefixed UTF-8
CString is (length::Int32, data::Ptr{UInt8}). It is length-prefixed and not null-terminated — embedded NUL bytes are permitted; length is the authoritative size. This makes it distinct from Base.Cstring, which is null-terminated and forbids embedded NULs.
As with the other types, CString borrows storage from the caller. The Python emitter recognizes it by name plus shape and generates from_str / as_str (UTF-8 round-trip) plus from_bytes / as_bytes (raw bytes) helpers.
CString <: AbstractString with ncodeunits, codeunit, valid- position checking, UTF-8 iteration, and a fast byte-level cmp. Base derives the rest: length (character count, distinct from ncodeunits), ==, print, regex matching, split, replace, … Call String(s) to copy the bytes out into a fresh, heap-allocated, owning Julia String.
JLWInterop.CString — Type
CStringABI-stable byte-string descriptor for crossing a @ccallable boundary: length bytes starting at data, interpreted as UTF-8. Length-prefixed, not null-terminated — embedded NUL bytes are permitted and length is the authoritative size. Distinct from Base.Cstring (which is null-terminated).
CString holds a raw pointer; it does not own the underlying buffer. Whoever allocated the storage (the caller passing the string in, or the library that returned it) remains responsible for keeping it alive for the entire time any CString referring to it is in use, and for freeing it afterward. CString performs no allocation, no copy, and no finalization.
Use it to expose a string value at a @ccallable boundary instead of a String (which is not C-ABI compatible) or a Cstring (which forces null-termination and forbids embedded NULs). The JuliaLibWrapping Python emitter recognizes CString by name + shape and emits from_str / as_str (UTF-8) plus from_bytes / as_bytes (raw) helpers on the corresponding ctypes.Structure.
CString <: AbstractString with ncodeunits, codeunit, and a fast byte-level cmp; Base derives UTF-8 iteration, length (character count vs. ncodeunits byte count), equality, print, regex matching, split, replace, and the rest of the AbstractString interface. Use String(s) to copy the bytes out into a fresh heap-allocated Julia String when you need ownership.
Example
using JLWInterop
Base.@ccallable function greeting_length(s::CString)::Int32
# AbstractString gives `length` (character count) directly; `s.length`
# is the byte count.
return Int32(length(s))
endThe caller (in Julia, Python, or C) is responsible for ensuring s.data points to at least s.length valid UTF-8 bytes for the duration of the call.