Date: 2026-07-31 Purpose: How the unified backend-agnostic LAPACK wrappers in src/linalg/lapack/ work, the contracts they share, and how to add a new routine. Module: linalg
This is the maintainer-level guide for the wrapper layer. For the user-facing Vector/Matrix API and everyday solver recipes, see linalg_usage_guide.md; this document covers what sits underneath: the ABI glue, the scratch-buffer contracts, and the conventions every fn_*.hpp in lapack/ follows.
Each LAPACK routine is wrapped exactly once, in a single header that works for both matrix backends (Armadillo and Blaze) and all four LAPACK datatypes (float, double, std::complex<float>, std::complex<double>). Each header contains three layers:
The shared machinery — datatype glue, integer policy, leading dimensions, and type traits — lives in lapacktools.hpp.
| Routine | Problem | Wrapper | Destroys / overwrites | Scratch |
|---|---|---|---|---|
| gesv | A·x = b, general square (LU) | gesv( A, b\|B, Pivot [, abort] ) | A → LU factors, b → solution | Pivot ≥ n |
| posv | A·x = b, SPD / Hermitian PD (Cholesky) | posv( A, b\|B [, abort] ) | A → Cholesky factor, b → solution | — |
| getrf | LU factorization | getrf( A, Pivot [, abort] ) | A → LU factors | Pivot (grown) |
| getri | inverse from LU | getri( A, Pivot, Work [, abort] ) | A → inverse | Work Vector<T> |
| gels | min ‖A·x − b‖, full rank | gels( A, b\|B, Work [, abort] ) | A → QR/LQ, b → solution | Work Vector<T> |
| gemm | C := α·op(A)·op(B) + β·C (BLAS) | gemm( A, B, C [, α, β, transa, transb] ) | C | — |
| gesvd | A = U·diag(S)·Vᵀ | gesvd( A, S, U, VT, Work [, jobs, abort] ) | A | Work Vector<real_t<T>> (carved) |
| geev | A·v = λ·v, general | geev( A, W, VL, VR, Work [, jobs, abort] ) | A | Work Vector<real_t<T>> (carved) |
| gees | Schur A = Z·T·Zᵀ/ᴴ | gees( A, W, VS, Work, BWork [, select, jobs, sdim, abort] ) | A → Schur form T | Work carved + BWork Vector<int_t> |
Executable examples for every routine and datatype: tests/linalg/test_lapack.cpp (typed gtest suite, 23 cases × 4 flavors).
Pivots are always Vector<int_t> — the element width must match the LAPACK integer (see Design Pillar 1). Two policies exist:
Scratch is always caller-owned and grow-only: the wrapper computes the documented minimum, asks LAPACK for the optimal size (lwork = -1 query) only when the provided buffer is too small, and otherwise uses the full buffer as-is. Reusing one Work object across calls therefore costs one allocation total. Three models exist:
(A) No Work — gesv, posv, getrf, gemm. The routine needs no workspace.
(B) Vector<T> Work — getri, gels. The work array uses the operand type.
(C) Single carved real Work — geev, gees, gesvd. One Vector< lapack::real_t<T> > carries everything, with no internal allocation:
The head is reinterpret_cast<T*>( Work.data() ) for complex T; a tRealsPerT constant (1 for real T, 2 for complex T) handles the length bookkeeping. The tail doubles as wr/wi for the real flavors — whose eigenvalues LAPACK returns as separate real and imaginary arrays, packed into the complex W afterwards — and as LAPACK's actual rwork for the complex flavors. Reading the optimal size from Work(0) works for both, since the real part of the first work entry comes first in memory.
Only owning Matrix/Vector objects may be passed — never Blaze views, submatrices, or expression objects. The wrappers take raw data() pointers plus a leading dimension; a view has neither a stable pointer nor the stride the helper computes.
Each pillar states one contract. Enforcement lives in lapacktools.hpp as static_asserts and preprocessor checks.
SCLS builds every third-party library in a given stack with a single integer width, so BELFEM's int_t and the linked LAPACK's integer coincide by construction. USE_MKL_64BIT_API switches both: BELFEM_INT64 (making int_t 64-bit) and MKL_ILP64; blaze_config.hpp derives BLAZE_BLAS_IS_64BIT from the same macro so Blaze's blas_int_t follows. Wrong: introducing a separate LAPACK integer typedef or declaring prototypes with plain int. Checked by: sizeof( int_t ) == sizeof( blaze::blas_int_t ) / sizeof( MKL_INT ) static_asserts and the MKL_ILP64 ↔ BELFEM_INT64 paired #error.
extern "C" prototypes must match any vendor headers that share the translation unit, or the compiler rejects the conflicting C-linkage declarations. Blaze's clapack headers declare the complex flavors with real-pair pointers (float*/double*) in every Blaze TU — also when MKL is the linked library (nothing defines INTEL_MKL_VERSION there). Therefore, cplx_*_t are MKL_Complex8/16 only for BELFEM_MKL && !BELFEM_BLAZE, and plain float/double otherwise. The dispatch specializations use reinterpret_cast from std::complex<T>*, which is layout-compatible by the C++11 array-access guarantee. Wrong: declaring prototypes with std::complex* (conflicts with Blaze) or with MKL_Complex* under Blaze.
It is the stride between columns of the stored array: n_rows() under Armadillo, matrix_data().spacing() under Blaze, whose columns are SIMD-padded so the stride may exceed the row count. It never depends on a transposition flag — trans only changes which logical dimension LAPACK validates the stride against, which the physical stride satisfies automatically. Wrong: passing n_rows() to LAPACK under Blaze (silent corruption once padding kicks in), or switching the stride on trans (wrong for every non-square matrix). Vectors are contiguous under both backends; their overload returns the logical length.
Fortran compilers append a hidden by-value length argument for every character dummy argument, after the regular argument list. All mainstream Linux compilers (gfortran, ifort/ifx, flang) share this convention; only the width varies. Under Blaze, fortran_charlen_t is Blaze's own typedef (int for gcc ≤ 7, size_t since gcc 8); under Armadillo BELFEM uses size_t unconditionally (lapacktools.hpp:97-101). Every prototype carries one fortran_charlen_t per char argument and every call passes 1. LAPACK itself never dereferences the lengths, so this is declaration compatibility (Blaze declares them) and ABI honesty, not a fix for observed breakage.
The dispatch templates take const pointers for the pure-input arguments; the extern "C" prototypes stay non-const because they must match the vendor declarations (Blaze declares non-const, MKL const — they cannot both be matched, and Blaze is the one that shares BELFEM TUs). The const_cast happens exactly once, inside the specializations.
| Pitfall | Consequence | Rule |
|---|---|---|
| Blaze view / submatrix passed to a wrapper | wrong stride, silent corruption | owning column-major objects only |
| mkl_lapack.h, the generic lapack.h, or un-shimmed slu_ddefs.h in the same TU | conflicting C-linkage declarations, compile error | never co-include vendor LAPACK/BLAS prototypes; SuperLU goes through the rename shim in cl_SolverSUPERLU.hpp |
| Forgetting that solvers destroy A | garbage on reuse | copy first if the original is still needed |
| gels RHS sized m in the underdetermined case | heap overrun | allocate max( m, n ) |
| Vector<int> pivots | ill-formed under BELFEM_INT64 | pivots are Vector<int_t> |
| Reading sdim as “number of true callback returns” (real gees) | off-by-pair | pairs count as two, see routine notes |