Skip to content

feat(ffi): primitive arrays cross cljw.ffi and wasm memory at memcpy speed (ADR-0204) - #82

Merged
BuddhiLW merged 5 commits into
stagingfrom
swarm/ffi-arrays
Oct 22, 2026
Merged

BuddhiLW merged 5 commits into
stagingfrom
swarm/ffi-arrays

Conversation

@BuddhiLW

Copy link
Copy Markdown
Owner

Primitive arrays cross cljw.ffi and wasm memory at memcpy speed

Kanban 20261021235518-022419a9 (CLJW-FFI-PRIM-ARRAY-BYTES). ADR-0204, recorded in memory as 20261022135049-25d05162.

What changed

  • Typed array storage. A Java array now carries its element kind. object-array holds Values and is traced. byte / short / int / long / float / double / char / boolean arrays hold raw contiguous native elements (i8 to f64, a u32 code point for char, u8 for boolean) and are untraced. The constructors are native (__array-make / __array-from / __array-fill) and convert values the way clj's RT.longArray does. aset is strict and raises clj's argument type mismatch.
  • cljw.ffi. A :pointer argument takes a primitive array and passes its storage, with no copy: C reads and writes it in place for the call. :bytes is now one memcpy.
  • wasm. wasm/mem-write! accepts a primitive array, and the new wasm/mem-read-array returns one. Each is one memcpy when the array's kind matches the dtype's layout; otherwise elements are converted one at a time. The code lives in a new memory_array.zig because surface.zig is already over the size ceiling.
  • Parity. bytes? is true only for a byte array, which retires AD-051. AD-019 now covers only make-array / into-array: cljw has no Class object to read an element kind from.
  • Docs. The docs/examples/ffi/README.md types table now includes the array-constructor-to-C-element table. The wasm and zig-native READMEs are updated.

Measurement

bench/ffi_prim_arrays.clj, 1000-element f64 dot, ReleaseSafe -Dwasm, on a loaded shared host, best of 5, µs per call:

route v1.15.3 this branch
:pointer + double-array, call refused 1.16
build both double-arrays + call n/a 29.1
:bytes, call only 19.6 1.73
pack doubles into :bytes + call 51,679 40,224
Clojure (reduce + (map * xs xs)) 2,174 2,472

Acceptance asked for the dot within 2x of the raw 12 µs call. The :pointer call takes 1.16 µs.

Tests

  • test/clj/suites/ffi_test.clj: double, int, float, long (C writes back) and byte arrays; :bytes copy semantics; 64 KiB byte array; arg-type refusals.
  • test/clj/suites/arrays_test.clj: per-kind storage, strict versus convert behaviour.
  • test/e2e/phase16_wasm_memory.sh: every array kind (i8 i16 i32 i64 f32 f64) round-trips, unsigned widening, a cross-kind conversion, three refusals.
  • test/e2e/phase14_arity_parity.sh: bytes? parity.

Ready for gate (run_gate.sh was not run locally; the host is shared).

A Java array now carries its element kind. object arrays hold Values and
are traced; byte/short/int/long/float/double/char/boolean arrays hold raw
contiguous native elements (i8 .. f64), untraced. A primitive array is a
C array as it stands, so cljw.ffi :pointer passes its storage with no
copy, :bytes is one memcpy, and socket I/O, String. and
System/arraycopy are memcpy/memmove.

Constructors are native (__array-make / __array-from / __array-fill),
converting like clj's RT.longArray; aset is strict (clj's argument type
mismatch). bytes? is true only for a byte array, retiring AD-051; AD-019
narrows to make-array / into-array, which have no Class to read a kind
from.

Kanban 20261021235518-022419a9.

Smell-audited: 3: type erasure removed at the container, Values unchanged (F-004/F-005 hold); one store path with strict/convert modes instead of per-ctor coerce fns
A :pointer argument takes a primitive array and passes its storage; C
reads and writes it in place for the call. The ffi suite covers double,
int, float, long (C write-back) and byte arrays, :bytes copy semantics,
and the arg-type refusals. README gains the full types table and the
array-constructor to C element table.

Kanban 20261021235518-022419a9.
wasm/mem-write! takes a primitive array as well as a vector: where the
array's kind is the dtype's layout (double-array and :f64, long-array
and :i64, ...) it is one memcpy, else element-wise with the vector
path's refusals. wasm/mem-read-array answers a primitive array, one
memcpy where the layout matches (:u16 / :u32 widen). New module
memory_array.zig keeps surface.zig from growing.

bench/ffi_prim_arrays.clj measures the 1000-element dot. ReleaseSafe
-Dwasm, loaded host, best of 5, us per call:
  v1.15.3   :bytes call only 19.6, pack + call 51,679, Clojure dot 2,174
  this      :pointer double-array call 1.16, build arrays + call 29.1,
            :bytes call only 1.73 (memcpy), pack + call 40,224

e2e phase16_wasm_memory covers every array kind (i8 i16 i32 i64 f32
f64), unsigned widening, a cross-kind convert and three refusals.

Kanban 20261021235518-022419a9.

Smell-audited: 2: bulk path split into its own module (surface.zig over the size ceiling); shared arg resolvers made pub rather than duplicated
java_array.zig: keep the typed per-kind make (ADR-0204) and charge its byte
buffer to the GC trigger with noteOwnedAlloc, as #79 does for every array
payload. data/placement.yaml regenerated (placement_drift was red on #82).

Smell-audited: 1: conflict resolution plus a regenerated index
@BuddhiLW
BuddhiLW merged commit 43c94fd into staging Oct 22, 2026
3 checks passed
@BuddhiLW
BuddhiLW deleted the swarm/ffi-arrays branch October 22, 2026 22:13
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.

1 participant