edgefirst_tensor_abi/lib.rs
1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Layout-only C ABI contract for the EdgeFirst tensor surface.
5//!
6//! Plain `#[repr(C)]` values and the integer vocabularies, declared exactly
7//! once. Every `-capi` crate imports these instead of carrying an "identical
8//! copy" — r2's duplication was safe but unverified-by-construction; a single
9//! declaration makes divergence impossible instead of merely absent.
10//!
11//! This crate has **no dependencies** and must never gain one. The vocabulary
12//! values are asserted against `edgefirst-tensor`'s authority from the crates
13//! that depend on both (see `edgefirst-tensor-capi/src/codes.rs`) — this
14//! crate is emitted surface, never a second authority.
15
16/// One plane's location, mirroring `TensorPlane` on the wire.
17///
18/// The route by which one library consumes a tensor another minted: the Rust
19/// types are not shared across `.so` boundaries, so the planes are the
20/// interface. Emitted into `edgefirst/tensor.h` as `ef_tensor_plane` by
21/// cbindgen.
22#[repr(C)]
23#[derive(Debug, Clone, Copy, Default)]
24pub struct EfTensorPlane {
25 /// dma-buf fd, IOSurface id, or -1 when host memory.
26 pub handle: i64,
27 /// Byte offset of this plane within the handle.
28 pub offset: u64,
29 /// Bytes per line.
30 pub stride: u64,
31 /// Plane extent in bytes.
32 pub size: u64,
33 /// Valid payload bytes.
34 pub used: u64,
35 /// DRM format modifier; 0 = linear.
36 pub modifier: u64,
37}
38
39/// Element type of a tensor's addressing grid.
40#[repr(u32)]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum EfDtype {
43 U8 = 0,
44 I8 = 1,
45 U16 = 2,
46 I16 = 3,
47 U32 = 4,
48 I32 = 5,
49 U64 = 6,
50 I64 = 7,
51 F16 = 8,
52 F32 = 9,
53 F64 = 10,
54}
55
56/// Backing store for a tensor.
57///
58/// Every kind is declared on every platform. Whether one can be *materialised*
59/// here is a runtime question — an IOSurface is a meaningful thing to name on
60/// Linux even though nothing will allocate one — and platform-gating the
61/// vocabulary would make the same integer mean different things per target.
62#[repr(u32)]
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum EfStorageKind {
65 Mem = 0,
66 Shm = 1,
67 DmaBuf = 2,
68 IoSurface = 3,
69 Pbo = 4,
70 Cuda = 5,
71 /// Windows `ID3D11Texture2D`, named specifically. A tensor's own
72 /// storage kind reports `DmaBuf` on Windows; this is the code the
73 /// cross-package descriptor carries for that backing.
74 D3d11Texture = 6,
75}
76
77/// CPU access direction for a map window.
78///
79/// Mirrors `edgefirst_tensor::CpuAccess`'s semantics; the values are the wire
80/// codes. `None` names the no-CPU-access declaration and is never a valid map
81/// direction.
82#[repr(u32)]
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum EfCpuAccess {
85 None = 0,
86 Read = 1,
87 Write = 2,
88 ReadWrite = 3,
89}
90
91/// Compression *request*, as `ef_tensor_image_desc_set_compression` takes it
92/// and [`EfImageDescView::compression`] reports it.
93///
94/// Distinct from [`EfCompression`], which names the scheme an allocation
95/// resolved to. No C setter can request `Scheme`; the view reports it so a
96/// request made through the Rust API is not folded into `Any` or dropped.
97#[repr(u32)]
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
99pub enum EfCompressionRequest {
100 /// No compression request: allocate linear.
101 #[default]
102 None = 0,
103 /// Any scheme the platform offers, linear when the format is not eligible.
104 Any = 1,
105 /// A specific vendor scheme, not further decodable through the view.
106 Scheme = 2,
107}
108
109/// Vendor tile-compression scheme actually in force for a tensor's
110/// allocation.
111///
112/// A *result*, not a request: [`EfImageDescView::compression`] carries what
113/// an allocation asked for ("any scheme" / "a specific one"), whereas this
114/// names the scheme the allocator actually resolved to. `None` is a real
115/// enumerator rather than a presence flag because "linear" is the answer for
116/// almost every allocation on almost every platform -- there is no absent
117/// case to distinguish from it, unlike `EfViewOrigin`/`EfQuantizationInfo`
118/// where every field value is legitimate.
119///
120/// Mirrors `edgefirst_tensor::CompressionScheme`'s variants, plus the
121/// `None` the Rust side spells as `Option::None`.
122#[repr(u32)]
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum EfCompression {
125 /// Linear layout -- no vendor tile compression.
126 #[default]
127 None = 0,
128 /// Qualcomm Adreno Universal Bandwidth Compression.
129 Ubwc = 1,
130 /// Arm Mali/Immortalis Framebuffer Compression.
131 Afbc = 2,
132 /// Imagination PowerVR Image Compression.
133 Pvric = 3,
134 /// Samsung Xclipse (AMD RDNA) Delta Color Compression.
135 Dcc = 4,
136}
137
138/// Whether a tensor's memory is known to be physically contiguous
139/// (`ef_tensor_contiguity`).
140///
141/// Mirrors `edgefirst_tensor::Contiguity`.
142#[repr(u32)]
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
144pub enum EfContiguity {
145 /// Not known: an imported fd, or memory not allocated from a DMA heap.
146 #[default]
147 Unknown = 0,
148 /// Allocated from the CMA heap.
149 Contiguous = 1,
150 /// Allocated from the system heap.
151 NonContiguous = 2,
152}
153
154/// Which *kind* of failure the calling thread's last failing
155/// `ef_tensor_*` call was.
156///
157/// The companion to `ef_tensor_last_error_message`, and the reason it
158/// exists: an entry point that reports failure by returning `NULL` has no
159/// errno to carry a class, so a Rust consumer rebuilding a typed
160/// `edgefirst_tensor::Error` from it had nothing to go on but the advisory
161/// message -- which the message's own contract says must never be parsed.
162/// `ef_tensor_batch` really did match on a fragment of a `Display` string
163/// for exactly this reason.
164///
165/// A **class**, not a one-to-one mirror of `edgefirst_tensor::Error`. Its
166/// variants are the distinctions a caller acts on differently; several Rust
167/// variants deliberately collapse into one here, and adding a Rust variant
168/// does not oblige adding one here. `Unspecified` is the honest answer when
169/// a failure path recorded only a message, and is what every such path
170/// resets this to -- a stale class from an earlier failure would be read as
171/// this call's, which is the confident-falsehood shape this whole mechanism
172/// exists to remove.
173#[repr(u32)]
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
175pub enum EfErrorClass {
176 /// No class recorded for this failure; the message is all there is.
177 #[default]
178 Unspecified = 0,
179 /// A caller argument was malformed, out of range, or not recognized.
180 InvalidArgument = 1,
181 /// A shape was rejected: wrong rank, wrong element count, or not a
182 /// shape the format admits.
183 InvalidShape = 2,
184 /// A shape or window did not fit the allocation behind it.
185 InsufficientCapacity = 3,
186 /// An index was outside the leading (batch) dimension.
187 BatchIndexOutOfBounds = 4,
188 /// A spatial region did not fit inside its parent frame.
189 RegionOutOfBounds = 5,
190 /// The operation is not available for this tensor's backing, on this
191 /// platform, or in this build.
192 NotSupported = 6,
193 /// The operation is legal but not permitted right now -- a live map, a
194 /// shared handle, a lock another holder owns.
195 InvalidOperation = 7,
196 /// An allocation failed, or a syscall backing one did.
197 AllocationFailed = 8,
198 /// A quantization payload was rejected.
199 QuantizationInvalid = 9,
200}
201
202/// Flattened, `#[repr(C)]` view of an image-request descriptor's fields.
203///
204/// `ef_tensor_image_desc` is opaque (handles are opaque both ways: a
205/// receiving library never dereferences, sizes, or copies one). This is the
206/// scalar block `ef_tensor_image_desc_get` fills instead -- the same shape as
207/// `ef_tensor_plane`, which lets one library read a tensor it did not mint
208/// without touching the other's private layout.
209///
210/// `memory` and `compression` are each a value plus an explicit presence
211/// flag rather than a sentinel: every code in `ef_storage_kind` (0..=6) is a
212/// real value, so there is no unused number to repurpose as "no request"
213/// without colliding with `ef_storage_kind`'s `MEM == 0`. `compression` is an
214/// [`EfCompressionRequest`]: `Any`, or `Scheme` for "a specific vendor
215/// scheme", the latter not further decodable through this view -- no
216/// `ef_tensor_image_desc_set_*` entry point can request one, so this view has
217/// never needed to carry more detail than "present, and it's a specific one."
218#[repr(C)]
219#[derive(Debug, Clone, Copy, Default)]
220pub struct EfImageDescView {
221 /// Requested width in pixels.
222 pub width: u64,
223 /// Requested height in pixels.
224 pub height: u64,
225 /// The requested pixel format's wire code (`PixelFormat::code()`).
226 pub format: u32,
227 /// `ef_dtype`.
228 pub dtype: u32,
229 /// `ef_cpu_access`.
230 pub access: u32,
231 /// `ef_storage_kind`, meaningful only when `has_memory != 0`.
232 pub memory: u32,
233 /// Non-zero when a specific memory backing was requested (`None` on the
234 /// Rust side auto-selects).
235 pub has_memory: u32,
236 /// `ef_compression_request`, meaningful only when `has_compression != 0`.
237 pub compression: u32,
238 /// Non-zero when a compression request was made.
239 pub has_compression: u32,
240}
241
242/// Parent-region snapshot for a tensor that is a `view`/`batch` sub-region.
243///
244/// The scalar block `ef_tensor_view_origin` fills, the same shape as
245/// `ef_tensor_plane` and `ef_tensor_image_desc_view` -- one library reading a
246/// tensor it did not mint. `has_origin` is a presence flag rather than a
247/// sentinel value because every field is a legitimate 0 for a view pinned at
248/// the parent's top-left corner; the other fields are meaningful only when
249/// it is non-zero. Mirrors `edgefirst_tensor::ViewOrigin`.
250#[repr(C)]
251#[derive(Debug, Clone, Copy, Default)]
252pub struct EfViewOrigin {
253 /// Logical width of the root parent image, in pixels.
254 pub parent_width: u64,
255 /// Logical height of the root parent image, in pixels.
256 pub parent_height: u64,
257 /// The parent's row stride in bytes.
258 pub parent_row_stride: u64,
259 /// This view's top-left x origin within the root parent, in pixels.
260 pub x: u64,
261 /// This view's top-left y origin within the root parent, in pixels.
262 pub y: u64,
263 /// Non-zero when this tensor is a view/batch sub-region; 0 for a whole
264 /// tensor, in which case the other fields are all zero and unused.
265 pub has_origin: u32,
266}
267
268/// The D3D11 texture behind a Windows texture tensor.
269///
270/// The scalar block `ef_tensor_d3d11_layout` fills, the same shape as
271/// `ef_tensor_plane` and `ef_tensor_view_origin` -- one library reading a
272/// tensor it did not mint. Mirrors `edgefirst_tensor::d3d11_layout::
273/// D3d11ImageLayout`.
274///
275/// The dimensions are the texture's, in texels and rows, not the image's in
276/// pixels: a semi-planar image is one texture whose row count covers both
277/// planes, and a YUYV image is one texel per two pixels, so
278/// `texture_width`/`texture_height` do not match `ef_tensor_shape` for
279/// either. Read the image's own dimensions from the shape.
280///
281/// For a semi-planar format (`nv12`, `nv16`, `nv24`) `texture_width` is the
282/// driver's row pitch -- at least `even(width)`, and on a discrete adapter
283/// commonly more (128 bytes on NVIDIA, so a 64-wide NV12 image is a 128-wide
284/// texture). It is the pitch the combined plane's rows are spaced by and the
285/// width a sampler must address the texture at. Never derive it from the
286/// image width: read this field, or `ef_tensor_row_stride`, which carries the
287/// same number.
288///
289/// By-value and frozen forever: a consumer bakes this size and these offsets
290/// into its call sites, so the struct evolves by a suffixed successor, never
291/// by an in-place edit. `d3d11_layout_is_pinned` and the C golden in
292/// `tensor-capi/tests/c/test_layout_goldens.c` hold both sides to it.
293#[repr(C)]
294#[derive(Debug, Clone, Copy, Default)]
295pub struct EfD3d11Layout {
296 /// `DXGI_FORMAT` the texture was created with.
297 pub dxgi_format: u32,
298 /// Texture width in texels.
299 pub texture_width: u32,
300 /// Texture height in rows.
301 pub texture_height: u32,
302 /// Bytes per texel of `dxgi_format`.
303 pub bytes_per_texel: u32,
304 /// The GL internal format an importer binds this texture as; 0 when the
305 /// format has no GL equivalent.
306 pub gl_internal_format: u32,
307}
308
309/// Presence/shape summary of a tensor's quantization metadata.
310///
311/// The first half of the two-call idiom `ef_tensor_quantization_info` /
312/// `ef_tensor_quantization_get` use: this scalar block tells the caller
313/// *whether* quantization is attached and *how big* a buffer the second call
314/// needs, without allocating on either side of the boundary --
315/// `Quantization` itself is variable-length (an axis plus per-axis `scales`
316/// and `zero_points`), so unlike `ef_tensor_plane`/`ef_tensor_view_origin`
317/// this view cannot carry the payload itself.
318///
319/// `has_quantization` is a presence flag, not a sentinel, for the same
320/// reason `EfViewOrigin::has_origin` is: axis `0` and a scale of `0.0` are
321/// both legitimate values, so there is no unused bit pattern to repurpose as
322/// "absent" without colliding with a real one.
323#[repr(C)]
324#[derive(Debug, Clone, Copy, Default)]
325pub struct EfQuantizationInfo {
326 /// Channel axis for per-channel quantization; `-1` for per-tensor (no
327 /// axis). Meaningful only when `has_quantization != 0`.
328 pub axis: i32,
329 /// Number of entries in the `scale`/`zero_point` arrays (1 for
330 /// per-tensor). Meaningful only when `has_quantization != 0`.
331 pub count: u32,
332 /// Non-zero when this tensor carries quantization metadata; 0 when it
333 /// does not, in which case `axis` and `count` are both zeroed and
334 /// unused.
335 pub has_quantization: u32,
336}
337
338/// A mapped CPU window over a tensor's bytes.
339///
340/// By-value with no version field and no reserved tail, so its size
341/// is baked into consumer call sites, so it evolves by a suffixed successor
342/// (`ef_tensor_view2` + new entry points), never in place. The pointer is
343/// valid from `ef_tensor_map` until the matching `ef_tensor_unmap`; writing
344/// through it is allowed only when the map was taken with a writable access.
345#[repr(C)]
346#[derive(Debug, Clone, Copy)]
347pub struct EfTensorView {
348 pub ptr: *mut u8,
349 pub len: usize,
350}
351
352/// A retain or release call on a client-owned callback context.
353///
354/// Must be safe to call from any thread and must not unwind.
355pub type EfClientRefFn = unsafe extern "C" fn(ctx: *const core::ffi::c_void);
356
357/// Client-owned state a library entry point calls back into — **frozen by
358/// value, forever**, and reused verbatim by every domain that needs one
359/// (PBO today; CUDA-GL next).
360///
361/// # What `retain`/`release` govern — and what they do not
362///
363/// They extend the life of the **callback channel only**. They never
364/// transfer ownership of the GL buffer, the CUDA resource, or anything else
365/// the context happens to address. Getting this backwards double-frees a GL
366/// buffer:
367///
368/// * The producing side's own destructor stays the sole caller of the real
369/// `delete_buffer`.
370/// * A library that reconstructed operations from this struct treats
371/// `delete_buffer` as a no-op — it borrows the resource, it does not own
372/// it.
373///
374/// # Ownership at an entry point
375///
376/// An entry point that stores this struct calls `retain(ctx)` itself and
377/// calls `release(ctx)` exactly once when the object holding it is
378/// destroyed. The caller keeps its own count and may drop it at any time
379/// after the call returns.
380///
381/// # Freezing
382///
383/// This vocabulary declines the `struct_size` handshake (see the root
384/// `ARCHITECTURE.md`, "Why there is no `struct_size` handshake"), so a
385/// by-value struct grows only by a major bump or a suffixed successor.
386/// Freezing **one** tiny universal struct — rather than one per domain, or
387/// one that grows per domain — is what keeps that cost paid once. Its size
388/// and offsets are pinned by `tests/c/test_layout_goldens.c` in
389/// `edgefirst-tensor-capi` and by `client_state_layout_is_pinned` below.
390///
391/// `ctx` is opaque to the library: only the producing copy of the code ever
392/// dereferences it.
393#[repr(C)]
394#[derive(Clone, Copy)]
395pub struct EfClientState {
396 /// Opaque to every caller except this struct's own function pointers.
397 pub ctx: *const core::ffi::c_void,
398 // Spelled out rather than as `Option<EfClientRefFn>` -- the identical
399 // type -- because cbindgen resolves `Option<T>` to a plain C function
400 // pointer only when `T` is syntactically a bare `fn`. Given the alias it
401 // emits an opaque wrapper struct and uses it *by value*, an incomplete
402 // type in C: the header would not even compile, let alone match this
403 // struct's 24 bytes. (A third form also works and is what the PBO ops
404 // use -- an alias whose right-hand side is itself `Option<bare fn>`;
405 // see `EfPboMapFnNullable`. It is not used here because these fields
406 // want no name of their own in the header.)
407 //
408 // This comment is `//`, not `///`, on purpose: it would otherwise be
409 // copied into the C header, where it would describe Rust machinery to a
410 // C reader and name a struct that does not exist in the emitted output.
411 /// Add one reference to the callback channel. Never NULL in a valid
412 /// state; an entry point receiving NULL must fail with `EINVAL`.
413 pub retain: Option<unsafe extern "C" fn(ctx: *const core::ffi::c_void)>,
414 /// Drop one reference to the callback channel. Never NULL in a valid
415 /// state; an entry point receiving NULL must fail with `EINVAL`.
416 pub release: Option<unsafe extern "C" fn(ctx: *const core::ffi::c_void)>,
417}
418
419/// Map a PBO for CPU access. `0` on success; `-1` when the GL context is
420/// gone; any other non-zero value a generic failure.
421pub type EfPboMapFn = unsafe extern "C" fn(
422 ctx: *const core::ffi::c_void,
423 buffer_id: u32,
424 size: usize,
425 out_ptr: *mut *mut u8,
426 out_len: *mut usize,
427) -> core::ffi::c_int;
428
429/// Unmap a PBO previously mapped by an [`EfPboMapFn`]. Same return codes.
430pub type EfPboUnmapFn =
431 unsafe extern "C" fn(ctx: *const core::ffi::c_void, buffer_id: u32) -> core::ffi::c_int;
432
433// This doc is what a C reader sees for `ef_pbo_map_fn`, so it says what the
434// callback does and nothing about Rust. The mechanics, for Rust readers:
435//
436// Two Rust aliases, one C type. In C there is only ever a function pointer
437// that may or may not be NULL, so this one carries the `ef_pbo_map_fn` name
438// in the header and the bare `EfPboMapFn` is not emitted at all. The bare
439// alias is the Rust-side vocabulary for a pointer already checked -- what
440// `PboOpsVtable` stores and what `client_state_pbo_ops` takes -- and it must
441// stay bare, or every holder would carry an `Option` it has already proved
442// is `Some`.
443//
444// This one is spelled as `Option<` over a *bare* `fn`, not over
445// `EfPboMapFn`: cbindgen resolves `Option<T>` to a nullable C function
446// pointer only when `T` is syntactically a bare `fn`. Given the alias it
447// emits an opaque `struct Option_EfPboMapFn` and passes it by value -- an
448// incomplete type no C caller can name. Given this, it emits
449// `typedef int (*ef_pbo_map_fn)(...)` and uses that name at the call site,
450// which is why `ef_tensor_wrap_pbo`'s header signature does not move.
451/// Map a PBO for CPU access. `0` on success; `-1` when the GL context is
452/// gone; any other non-zero value a generic failure.
453///
454/// May be NULL where an entry point takes one; such an entry point refuses
455/// a NULL rather than calling through it.
456pub type EfPboMapFnNullable = Option<
457 unsafe extern "C" fn(
458 ctx: *const core::ffi::c_void,
459 buffer_id: u32,
460 size: usize,
461 out_ptr: *mut *mut u8,
462 out_len: *mut usize,
463 ) -> core::ffi::c_int,
464>;
465
466// See `EfPboMapFnNullable` for why there are two aliases and why this one
467// spells the `fn` out rather than naming `EfPboUnmapFn`.
468/// Unmap a PBO previously mapped by an `ef_pbo_map_fn`. Same return codes.
469///
470/// May be NULL where an entry point takes one; such an entry point refuses
471/// a NULL rather than calling through it.
472pub type EfPboUnmapFnNullable =
473 Option<unsafe extern "C" fn(ctx: *const core::ffi::c_void, buffer_id: u32) -> core::ffi::c_int>;
474
475/// Map a CUDA graphics resource to a device pointer. `0` on success.
476pub type EfCudaMapFn = unsafe extern "C" fn(
477 ctx: *const core::ffi::c_void,
478 resource: *mut core::ffi::c_void,
479 out_ptr: *mut *mut core::ffi::c_void,
480 out_len: *mut usize,
481) -> core::ffi::c_int;
482
483/// Unmap a resource previously mapped by an [`EfCudaMapFn`].
484pub type EfCudaUnmapFn =
485 unsafe extern "C" fn(ctx: *const core::ffi::c_void, resource: *mut core::ffi::c_void);
486
487/// Unregister a CUDA graphics resource.
488pub type EfCudaUnregisterFn =
489 unsafe extern "C" fn(ctx: *const core::ffi::c_void, resource: *mut core::ffi::c_void);
490
491/// Map a CUDA graphics resource to a device pointer. `0` on success.
492///
493/// May be NULL where an entry point takes one; such an entry point refuses
494/// a NULL rather than calling through it.
495pub type EfCudaMapFnNullable = Option<
496 unsafe extern "C" fn(
497 ctx: *const core::ffi::c_void,
498 resource: *mut core::ffi::c_void,
499 out_ptr: *mut *mut core::ffi::c_void,
500 out_len: *mut usize,
501 ) -> core::ffi::c_int,
502>;
503
504/// Unmap a resource previously mapped by an `ef_cuda_map_fn`.
505///
506/// May be NULL where an entry point takes one; such an entry point refuses
507/// a NULL rather than calling through it.
508pub type EfCudaUnmapFnNullable =
509 Option<unsafe extern "C" fn(ctx: *const core::ffi::c_void, resource: *mut core::ffi::c_void)>;
510
511/// Unregister a CUDA graphics resource.
512///
513/// May be NULL where an entry point takes one; such an entry point refuses
514/// a NULL rather than calling through it.
515pub type EfCudaUnregisterFnNullable =
516 Option<unsafe extern "C" fn(ctx: *const core::ffi::c_void, resource: *mut core::ffi::c_void)>;
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn plane_layout_is_the_48_byte_scalar_block_the_blob_assumes() {
524 // The blob's 56-byte plane record is this 48-byte scalar block plus
525 // two 4-byte sequence counts (spec: Blob format, [r2] As built).
526 assert_eq!(std::mem::size_of::<EfTensorPlane>(), 48);
527 assert_eq!(std::mem::align_of::<EfTensorPlane>(), 8);
528 }
529
530 #[test]
531 fn vocabulary_enums_are_u32() {
532 assert_eq!(std::mem::size_of::<EfDtype>(), 4);
533 assert_eq!(std::mem::size_of::<EfStorageKind>(), 4);
534 assert_eq!(std::mem::size_of::<EfCompressionRequest>(), 4);
535 }
536
537 #[test]
538 fn tensor_view_is_two_pointers_wide_and_frozen() {
539 assert_eq!(std::mem::size_of::<EfTensorView>(), 16);
540 assert_eq!(std::mem::align_of::<EfTensorView>(), 8);
541 }
542
543 #[test]
544 fn image_desc_view_layout_is_pinned() {
545 // Two `u64`s force 8-byte alignment, so the seven trailing `u32`s
546 // (28 bytes) pad out to a 48-byte struct. Pinned so a field reorder
547 // or an added field is a deliberate, visible change here.
548 assert_eq!(std::mem::size_of::<EfImageDescView>(), 48);
549 assert_eq!(std::mem::align_of::<EfImageDescView>(), 8);
550 }
551
552 #[test]
553 fn view_origin_layout_is_pinned() {
554 // Five `u64`s plus a trailing `u32` pad out to 48 at align 8. Same
555 // freeze as the C golden: a field reorder is a new suffixed struct.
556 assert_eq!(std::mem::size_of::<EfViewOrigin>(), 48);
557 assert_eq!(std::mem::align_of::<EfViewOrigin>(), 8);
558 }
559
560 #[test]
561 fn error_class_is_u32_and_unspecified_is_zero() {
562 assert_eq!(std::mem::size_of::<EfErrorClass>(), 4);
563 // 0 must be the "nothing recorded" answer: a zeroed or never-set
564 // slot has to read as "no class", never as a real classification a
565 // consumer would then act on.
566 assert_eq!(EfErrorClass::Unspecified as u32, 0);
567 assert_eq!(EfErrorClass::default(), EfErrorClass::Unspecified);
568 }
569
570 #[test]
571 fn compression_is_u32_and_none_is_zero() {
572 assert_eq!(std::mem::size_of::<EfCompression>(), 4);
573 assert_eq!(std::mem::size_of::<EfContiguity>(), 4);
574 // `ef_tensor_compression` returns this by value, and a zeroed/failed
575 // read must read as "linear", never as a scheme.
576 assert_eq!(EfCompression::None as u32, 0);
577 assert_eq!(EfCompression::default(), EfCompression::None);
578 }
579
580 #[test]
581 fn cpu_access_is_u32() {
582 assert_eq!(std::mem::size_of::<EfCpuAccess>(), 4);
583 }
584
585 #[test]
586 fn d3d11_layout_is_pinned() {
587 // Five 4-byte fields, no `u64` to force wider alignment -- 20 bytes,
588 // 4-byte aligned, and the field order the C golden asserts offsets
589 // for. A widened field or a reorder is a new suffixed struct.
590 assert_eq!(std::mem::size_of::<EfD3d11Layout>(), 20);
591 assert_eq!(std::mem::align_of::<EfD3d11Layout>(), 4);
592 }
593
594 #[test]
595 fn quantization_info_layout_is_pinned() {
596 // Three 4-byte fields, no `u64` to force wider alignment -- 12 bytes,
597 // 4-byte aligned. Pinned so a field reorder or an added field is a
598 // deliberate, visible change here.
599 assert_eq!(std::mem::size_of::<EfQuantizationInfo>(), 12);
600 assert_eq!(std::mem::align_of::<EfQuantizationInfo>(), 4);
601 }
602
603 #[test]
604 fn a_nullable_pbo_op_is_one_pointer_and_matches_its_bare_alias() {
605 // The nullable aliases are what `ef_tensor_wrap_pbo` receives and
606 // what the header's `ef_pbo_map_fn` typedef describes. If the
607 // null-pointer optimization did not apply they would not be plain C
608 // function pointers, and the header would describe an argument the
609 // ABI does not pass.
610 assert_eq!(
611 core::mem::size_of::<EfPboMapFnNullable>(),
612 core::mem::size_of::<EfPboMapFn>()
613 );
614 assert_eq!(
615 core::mem::size_of::<EfPboUnmapFnNullable>(),
616 core::mem::size_of::<EfPboUnmapFn>()
617 );
618 assert_eq!(
619 core::mem::size_of::<EfPboMapFnNullable>(),
620 core::mem::size_of::<*const core::ffi::c_void>()
621 );
622 // And `Some(f)` really is `f`: the two aliases describe one C type,
623 // so these assignments must compile.
624 unsafe extern "C" fn m(
625 _: *const core::ffi::c_void,
626 _: u32,
627 _: usize,
628 _: *mut *mut u8,
629 _: *mut usize,
630 ) -> core::ffi::c_int {
631 0
632 }
633 unsafe extern "C" fn u(_: *const core::ffi::c_void, _: u32) -> core::ffi::c_int {
634 0
635 }
636 let f: EfPboMapFn = m;
637 let _: EfPboMapFnNullable = Some(f);
638 let g: EfPboUnmapFn = u;
639 let _: EfPboUnmapFnNullable = Some(g);
640 }
641
642 #[test]
643 fn client_state_layout_is_pinned() {
644 // Three pointer-sized members at align 8. Frozen forever: this
645 // vocabulary declines a `struct_size` handshake, so a field added
646 // here is a new suffixed struct, not an edit. Mirrored by
647 // `_Static_assert`s in edgefirst-tensor-capi's C layout goldens.
648 assert_eq!(std::mem::size_of::<EfClientState>(), 24);
649 assert_eq!(std::mem::align_of::<EfClientState>(), 8);
650 assert_eq!(std::mem::offset_of!(EfClientState, ctx), 0);
651 assert_eq!(std::mem::offset_of!(EfClientState, retain), 8);
652 assert_eq!(std::mem::offset_of!(EfClientState, release), 16);
653 }
654
655 #[test]
656 fn a_nullable_client_ref_fn_is_pointer_sized() {
657 // `Option<extern "C" fn(..)>` must use the null-pointer
658 // optimization, or `retain`/`release` would not be a plain C
659 // function pointer and the struct above would not be 24 bytes.
660 // `EfClientRefFn` is the same type the two fields spell out (they
661 // spell it out only so cbindgen emits a C function pointer rather
662 // than an opaque `Option_` struct), so this covers both.
663 assert_eq!(
664 std::mem::size_of::<Option<EfClientRefFn>>(),
665 std::mem::size_of::<*const core::ffi::c_void>()
666 );
667 // And they really are the same type, not merely the same size: this
668 // assignment would not compile otherwise.
669 let _: Option<EfClientRefFn> = EfClientState {
670 ctx: core::ptr::null(),
671 retain: None,
672 release: None,
673 }
674 .retain;
675 }
676
677 #[test]
678 fn this_crate_has_no_dependencies_and_never_may() {
679 // Load-bearing for the whole design: tensor-ffi -> tensor-abi must never
680 // drag an implementation into the dynamic backend (spec r3). The
681 // [dependencies] table is empty and this test keeps it that way.
682 let manifest = include_str!("../Cargo.toml");
683 assert!(
684 !manifest.contains("[dependencies."),
685 "dependency sub-tables are dependencies too"
686 );
687 let deps = manifest
688 .split("[dependencies]")
689 .nth(1)
690 .expect("manifest has a [dependencies] table");
691 let table_body = deps.split("\n[").next().unwrap_or(deps);
692 let non_comment_lines: Vec<&str> = table_body
693 .lines()
694 .map(str::trim)
695 .filter(|l| !l.is_empty() && !l.starts_with('#'))
696 .collect();
697 assert!(
698 non_comment_lines.is_empty(),
699 "edgefirst-tensor-abi grew dependencies: {non_comment_lines:?}"
700 );
701 }
702}