Skip to main content

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