Skip to content

Add FFI_TYPE_VECTOR: vector (SIMD) type support with libffi-computed layout#1000

Open
edusperoni wants to merge 5 commits into
libffi:masterfrom
edusperoni:feat/vector-types
Open

Add FFI_TYPE_VECTOR: vector (SIMD) type support with libffi-computed layout#1000
edusperoni wants to merge 5 commits into
libffi:masterfrom
edusperoni:feat/vector-types

Conversation

@edusperoni

@edusperoni edusperoni commented Jul 21, 2026

Copy link
Copy Markdown

This PR adds a portable API for marshalling vector (SIMD) types — the values produced by GCC's __attribute__((vector_size(N))) and Clang's ext_vector_type — with ports for aarch64 and x86-64.

It supersedes #414 (open since 2018) and is designed as a direct answer to the design questions Anthony raised on the mailing list at the time (https://sourceware.org/legacy-ml/libffi-discuss/2018/msg00020.html), which is where that PR stalled. It also addresses #773 (aarch64 vec4 return values), whose reporter had no way to describe float32x4_t to libffi.

The design questions, answered

The 2018 objection to #414 was that it required callers to fill in a vector's size and alignment by hand, breaking libffi's promise to compute type layout itself, and that GCC and Clang disagree about vector alignment. This PR takes the third option floated in that thread — "only require the definition of one element type":

  • A vector is described exactly like a struct: type = FFI_TYPE_VECTOR and a NULL-terminated elements[] array — except every element must point to the same fundamental scalar (float, double, or a fixed-width integer), and the count is the number of lanes. No new struct fields, no ABI change to ffi_type.
  • The caller leaves size/alignment at zero; ffi_prep_cif computes them: size = lane_size × lanes, rounded up to the next power of two (this is Clang's ext_vector_type storage rule — e.g. 3 × float → 16; GCC's vector_size already requires power-of-two totals, so the rule is identical there); alignment = min(size, 16).
  • On the GCC-vs-Clang concern: at the call boundary the platform psABI (AAPCS64, SysV x86-64) defines vector passing regardless of which compiler built either side. The attribute-alignment differences affect in-memory layout only, which this API sidesteps by computing storage from the element type. This is spelled out in the new documentation.

Invalid descriptions (heterogeneous lanes, aggregate lanes, long double, zero lanes) return FFI_BAD_TYPEDEF.

Structure — the FFI_TYPE_COMPLEX precedent

The feature follows the same shape as complex-type support:

  • FFI_TYPE_VECTOR = 18 appended after FFI_TYPE_SINT128, FFI_TYPE_LAST bumped; defined unconditionally (no configure gating).
  • Ports opt in via FFI_TARGET_HAS_VECTOR_TYPE in ffitarget.h. On any port that doesn't, ffi_prep_cif returns FFI_BAD_TYPEDEF for vector signatures (including vectors nested in structs) — a clean error, never an abort.
  • Plumbed through raw_api.c / java_raw_api.c / debug.c alongside FFI_TYPE_STRUCT, mirroring complex.

Ports

aarch64 (AAPCS64): 8/16-byte short vectors in single V/Q registers (float, double, and integer lanes); homogeneous vector aggregates (structs of 2–4 identical short vectors) in consecutive Q registers; bare vectors wider than 16 bytes by reference, matching compiler codegen (verified against Clang). Closures handled symmetrically.

x86-64 (SysV psABI): additive FFI_TYPE_VECTOR case in classify_argument — 8-byte vectors classify as one SSE eightbyte, 16-byte as SSE+SSEUP (one %xmm); returns in %xmm0. Vectors wider than 16 bytes are rejected with FFI_BAD_TYPEDEF rather than passed subtly wrong: correct %ymm/%zmm passing needs register-save changes in unix64.S and is left as a documented follow-up. Existing classification (including the recent UINT128/SINT128 support) is untouched.

Tests and docs

  • New testsuite/libffi.vector/ modeled on libffi.complex/ (same driver and target-skip mechanism): pass/return of float32x2/x4 (the On Android platform aarch64 ABI does not return vec4 correctly #773 shape), double2, int32x4; mixed scalar/vector calls with stack spill; the Clang 3-lane padding case; 32-byte double4 on aarch64; HVA structs; closures both directions; and negative tests for the validation and the x86-64 width limit.
  • Results: 40/40 pass on aarch64 (macOS, native AAPCS64) and 40/40 on x86-64; full existing testsuite passes with no regressions on both.
  • New "Vector Types" node in libffi.texi documenting the API, the layout rule, the psABI framing, and the per-port support matrix.

Provenance

The aarch64 marshalling logic derives from the NativeScript iOS runtime's production libffi fork, which has shipped vector marshalling for Objective-C simd/SceneKit types since 2019. The runtime has now been ported to this exact API (FFI_TYPE_VECTOR with lane-count elements[] and computed layout) and passes its full test suite against this branch on arm64 device and simulator — so this PR is battle-tested marshalling reworked to the computed-layout design asked for in 2018, and merging it lets that fork be retired in favor of upstream.

Happy to adjust the API surface or split/squash the series however you prefer.

cc @ronaldoussoren (PyObjC interest on #414), refs #414, closes #773

Introduce a portable API for marshalling vector (SIMD) types -- the
values produced by GCC's __attribute__((vector_size)) and Clang's
ext_vector_type.  This answers the stalled PR libffi#414 and the maintainer's
2018 design questions
(https://sourceware.org/legacy-ml/libffi-discuss/2018/msg00020.html):
rather than requiring callers to hand-compute a vector's size and
alignment (and gating the feature behind configure), libffi now derives
the layout itself and the type code is defined unconditionally.

A vector is described exactly like a struct: type == FFI_TYPE_VECTOR and
a NULL-terminated elements[] array, except every element must point to
the SAME fundamental scalar (float, double, or a fixed-width integer
UINT8..SINT64) and the count is the number of lanes.  The caller leaves
size and alignment at zero; ffi_prep_cif computes:

  size      = lane_size * count, rounded up to the next power of two
              (matches Clang ext_vector_type storage: 3 x float -> 16,
              3 x double -> 32; GCC vector_size already requires pow2
              totals so it is identical there);
  alignment = min(size, 16).

Validation (identical scalar lanes, count >= 1, scalar-only) yields
FFI_BAD_TYPEDEF otherwise.

  - include/ffi.h.in: FFI_TYPE_VECTOR = 18 (after SINT128 = 17),
    FFI_TYPE_LAST bumped.  Defined unconditionally, no configure gating.
  - src/prep_cif.c: initialize_vector() computes the layout in
    initialize_aggregate; ffi_type_contains_vector() rejects vectors
    (including nested in structs, argument or return) with
    FFI_BAD_TYPEDEF on any port that does not define
    FFI_TARGET_HAS_VECTOR_TYPE -- no aborts.  Vector returns reserve the
    hidden return-pointer slot like structs.
  - src/raw_api.c, src/java_raw_api.c: plumb FFI_TYPE_VECTOR alongside
    FFI_TYPE_STRUCT, mirroring how FFI_TYPE_COMPLEX is handled.
  - src/debug.c: ffi_type_test requires elements != NULL for vectors.
  - src/pa/ffitarget.h: bump the FFI_PA_TYPE_LAST tripwire; PA gates
    vectors out in prep_cif so its jump tables are never reached.
  - doc/libffi.texi: new "Vector Types" node documenting the API, the
    computed-layout rule, the psABI framing, and the per-port support
    table.

No port defines FFI_TARGET_HAS_VECTOR_TYPE yet, so this commit rejects
every vector signature; the per-architecture ports follow.

References: libffi#414, libffi#773.
Define FFI_TARGET_HAS_VECTOR_TYPE for AArch64 and teach is_vfp_type to
classify FFI_TYPE_VECTOR, so ffi_call and closures pass and return
vectors the way AAPCS64 (and current GCC/Clang) do:

  - 8- and 16-byte vectors travel in a single V/Q register (a Short
    Vector), for float, double and integer lane types alike;
  - homogeneous vector aggregates -- a struct of up to four identical
    8- or 16-byte vectors -- travel in that many consecutive V/Q
    registers (an HVA), e.g. struct{float32x4 a,b} in {q0,q1};
  - a bare vector wider than 16 bytes (e.g. a 32-byte double4) has no
    short-vector register class, so is_vfp_type returns 0 and the
    existing composite path passes it by invisible reference and returns
    it in memory -- exactly what a natively compiled callee expects.

is_simd() reports the width of one Neon register slot (a bare vector's
whole size, or one lane vector of an HVA); is_vfp_type() encodes
num_registers slots of that width onto the existing AARCH64_RET_{D,Q}*
codes via intlog2.  is_hfa0/is_hfa1 recurse through FFI_TYPE_VECTOR so
HVA homogeneity is checked, and the three fundamental-type dispatch
switches (machdep return, ffi_call_int, ffi_closure_SYSV_inner) route
FFI_TYPE_VECTOR through is_vfp_type alongside FFI_TYPE_STRUCT.

Ported from the battle-tested NativeScript aarch64 vector marshaller,
adapted to the FFI_TYPE_VECTOR API and extended so that integer-lane
vectors (e.g. int32x4) are classified into V registers too -- the
original only handled floating-point lanes.

References: libffi#414, libffi#773 (aarch64 vec4 return).
Define FFI_TARGET_HAS_VECTOR_TYPE for the SysV x86-64 backend (ffi64.c;
32-bit x86 and the Windows ffiw64.c backend are excluded) and integrate
FFI_TYPE_VECTOR into the existing psABI classifier without restructuring
it:

  - classify_argument gains a FFI_TYPE_VECTOR case: an 8-byte vector is
    one SSE eightbyte (X86_64_SSE_CLASS); a 16-byte vector is one %xmm
    register (X86_64_SSE_CLASS + X86_64_SSEUP_CLASS).  The existing
    INTEGERSI/SSESF/SSEDF/UINT128 handling is untouched, and the SSE+SSEUP
    argument marshalling already merges both eightbytes into one %xmm.
  - ffi_prep_cif_machdep classifies vector returns symmetrically: 8 bytes
    in %xmm0 (UNIX64_RET_XMM64), 16 bytes in %xmm0 (UNIX64_RET_XMM128).
  - Vectors wider than 16 bytes return FFI_BAD_TYPEDEF from
    ffi_prep_cif_machdep, for both returns and arguments.  Correct
    %ymm/%zmm passing needs unix64.S register-save changes and is left as
    a v1 limitation rather than silently passing them in memory.

Closures need no separate change: the closure paths reuse
classify_argument for arguments and cif->flags for the return.

References: libffi#414.
Model a new testsuite/libffi.vector/ directory on testsuite/libffi.complex:
vector.exp reuses the same dg/run-many-tests driver and skips every test as
"unsupported" on ports whose headers do not define
FFI_TARGET_HAS_VECTOR_TYPE (libffi_feature_test), so unsupported targets
still compile the gating cleanly.

Vector types are built with the portable __attribute__((vector_size)) via a
small make_vector_type() helper (vector.h); each test cross-checks the value
returned through ffi against a direct native call.  Coverage:

  - vector_float32x4 / vector_float32x2 / vector_double2 / vector_int32x4:
    pass and return 8- and 16-byte float, double and integer vectors
    (float32x4 is the vec4 shape of libffi#773);
  - vector_args_spill: ten vectors interleaved with int/double scalars,
    exhausting the vector argument registers and spilling to the stack;
  - vector_vec3: Clang-only ext_vector_type(3), verifying the 12->16 byte
    power-of-two padding matches a natively compiled callee (a no-op on
    other compilers);
  - vector_double4: on AArch64 a 32-byte vector round-trips (by reference /
    in memory); elsewhere ffi_prep_cif must return FFI_BAD_TYPEDEF, checked
    for both return and argument;
  - vector_hva: a struct of two identical vectors (HVA) passes and returns
    on both AArch64 (Q-register pair) and x86-64 (SSE struct classification);
  - cls_vector: a closure receiving vector arguments and returning a vector;
  - vector_validate: heterogeneous lanes, an empty vector, and a non-scalar
    lane are each rejected with FFI_BAD_TYPEDEF, and a well-formed vector is
    accepted with the computed power-of-two size and min(size,16) alignment.

The files are added to testsuite/Makefile.am EXTRA_DIST, matching how
libffi.complex is distributed.

References: libffi#414, libffi#773.
Two fixes for the libffi.vector suite:

- vector_double4.c: the non-aarch64 branch built its own void_args
  array and never read the already-populated args, tripping gcc's
  -Wunused-but-set-variable (an excess-errors FAIL on Linux x86-64
  with gcc; clang does not emit this warning).  Use args for the
  negative argument-passing check instead.

- vector.exp: the suite only probed FFI_TARGET_HAS_VECTOR_TYPE, but
  the tests are written with the GCC/Clang vector extension.  On
  Windows ARM64 the aarch64 port enables the feature while MSVC
  cannot compile __attribute__ ((vector_size)), so every test failed
  to build.  Add a compile probe and mark the suite unsupported when
  the compiler lacks the syntax.
@edusperoni

Copy link
Copy Markdown
Author

Pushed 19dbdb5 with two testsuite fixes:

  • vector_double4.c: silence a gcc-only -Wunused-but-set-variable warning
    that failed the excess-errors check on the Linux x86_64 gcc jobs (execution
    always passed).
  • vector.exp: skip the suite when the compiler can't build GCC/Clang vector
    syntax — fixes the Windows ARM64 MSVC job, where the aarch64 port enables
    the feature but MSVC can't compile the tests.

The Windows 64-bit MSVC failures look unrelated to this PR: they're all
pre-existing struct/closure tests, and no vector code runs on X86_WIN64 (the
feature isn't enabled there). Master is green at this PR's base SHA, but on an
older runner image (20260628.158.1 vs 20260714.173.1 here) — a master
re-run on the current image should confirm.

@edusperoni

Copy link
Copy Markdown
Author

@atgreen can you approve the workflows again so we're green?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

On Android platform aarch64 ABI does not return vec4 correctly

1 participant