Skip to content

feat(ffi): structs that can grow, plane sizes, new statuses, versions - #153

Merged
with-developer merged 2 commits into
mainfrom
feat/152-c-abi
Oct 31, 2026
Merged

with-developer merged 2 commits into
mainfrom
feat/152-c-abi

Conversation

@with-developer

@with-developer with-developer commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Purpose

From 1.0 on the C API is to stay compatible. An audit of v0.16.0 by our review agent found that it could not grow without breaking programs (#152). This pull request breaks it once, before 1.0, so that it need not break again. It is the C half of #152; the Rust half follows.

Changes

  • Structs that can grow (D69). Every struct passed by pointer starts with struct_size, which the caller sets to sizeof the struct, for structs in and out.
    • emberwave reads and writes through read_struct and write_struct: no more bytes than the size, and zeros for the members a shorter struct lacks.
    • A size below the struct's first size is EMBERWAVE_STATUS_INVALID_ARGUMENT.
    • EmberwaveVulkanDevice points to its instance. EmberwaveSignalling's reserved words go.
    • A Default sets the size, for Rust callers and the tests.
  • Plane sizes. EmberwavePlane and EmberwaveSourcePlane carry size, which the calls check.
  • Statuses.
    • EMBERWAVE_STATUS_UNSUPPORTED replaces NO_GPU for a call the system lacks.
    • EMBERWAVE_STATUS_BUFFER_TOO_SMALL replaces INVALID_ARGUMENT for an output array, a decode's plane or an encode's budget that is too small.
    • The header says to treat any status but OK as a failure.
  • One array convention. emberwave_vulkan_instance_extensions takes a capacity apart from the count, as emberwave_frame_packets does. Its counts stay uint32_t, as Vulkan's are.
  • Names. Metal's submits are emberwave_decoder_submit_texture_metal and emberwave_decoder_submit_plane_textures_metal.
  • Versions. EMBERWAVE_VERSION_MAJOR, _MINOR, _PATCH and EMBERWAVE_VERSION in the header, and emberwave_version_number() in the library. A compile-time assertion ties them to the crate's version.
  • Threads. Compile-time assertions:
    • in the C API, contexts Send + Sync, and decoders, encoders and textures Send;
    • in the core, outside wasm32, GpuContext, Decoder and Encoder.
  • Docs.
    • The header's conventions: sizes, statuses, callbacks that must not call emberwave, versions, and the interop calls marked provisional.
    • The C README: a "Versions, sizes and statuses" section with a table of platform tiers, and every snippet updated.
    • ARCHITECTURE.md: the C API section and D69.
    • CONTRIBUTING.md: the struct rule and the release rule.
    • CHANGELOG.md.
  • Callers updated: the C examples, the Swift example, the benchmark app, and every test (api.rs, encoder.rs, metal.rs, vulkan.rs, d3d12.rs).

Testing

  • On the M3 Max (Metal):
    • cargo test --release --workspace, with new tests for a size below the first (refused, nothing written), a larger struct (filled up to what this version knows, the rest untouched), a zeroed options struct, a decode's plane too small, an encode's source plane too small, and the header's version against the library's.
    • scripts/check_c_example.sh: the C examples decode every vector and encode two sources.
    • scripts/bench_app_on_mac.sh -autorun: the benchmark app's texture, plane and queue checks pass.
  • Windows check: cargo clippy --target x86_64-pc-windows-msvc --workspace --all-targets -D warnings compiles the Vulkan and Direct3D code and their tests (vulkan.rs, d3d12.rs) without a warning. They run in CI on lavapipe and WARP. A new Vulkan test checks a names array one short.
  • Lints: cargo fmt --check, workspace clippy, --no-default-features, and wasm32.
  • CI only: the iOS slices and the Swift example. Rust's iOS targets are not installed here.

Benchmark impact

None.

Review

Our review agent found no unsound code and no functional bug. It probed the helpers from C, with oversized structs, sizes 0 to 11 and NULL, and plane spans with canary bytes. It compiled the header as C99 to C17 and C++11 and C++17, and type-checked the Swift sources. It found two flaws in the growth scheme and stale docs, all fixed in 3d7cf7c:

  • Tail padding. EmberwaveVulkanInstance and EmberwaveVulkanSync ended with four bytes of padding. A member added there would have left struct_size unchanged, and the library would have read it from an older caller's padding. Their 32-bit members now come first.
  • First sizes. FIRST_SIZE was size_of, so the first member added would have refused every older program. The macro now takes the last member of each struct's first layout, and asserts at compile time that struct_size comes first and that no padding follows the last member. A test pins the first sizes.
  • Docs. They named INVALID_ARGUMENT for a budget too small and NO_GPU off-platform; they now name BUFFER_TOO_SMALL and UNSUPPORTED. The enum's doc now says which calls give UNSUPPORTED. The plane contracts name size, and the Vulkan device contract names its instance.
  • Coverage. A Vulkan test now covers a NULL or short instance.

Related issue

Part of #152.

From 1.0 on the C API is to stay compatible, and an audit of v0.16.0
found that it could not grow without breaking programs (#152, D69). This
breaks it once, before 1.0, so that it need not break again:

- Every struct passed by pointer starts with struct_size, which the
  caller sets to sizeof the struct. emberwave reads and writes no more
  than that, and takes the members a shorter struct lacks as zeros, so a
  minor version can add a member at the end. A size below the struct's
  first is INVALID_ARGUMENT. EmberwaveVulkanDevice points to its instance
  now, and EmberwaveSignalling's reserved words go.
- EmberwavePlane and EmberwaveSourcePlane carry size, the bytes at data,
  which the calls check, instead of trusting the stride and the rows.
- EMBERWAVE_STATUS_UNSUPPORTED for a call the system lacks, which was
  NO_GPU, and EMBERWAVE_STATUS_BUFFER_TOO_SMALL for an output array,
  plane or budget too small, which was INVALID_ARGUMENT. The header says
  to treat any status but OK as a failure.
- emberwave_vulkan_instance_extensions takes a capacity apart from the
  count, as emberwave_frame_packets does.
- Metal's two submits are emberwave_decoder_submit_texture_metal and
  emberwave_decoder_submit_plane_textures_metal, as Vulkan's and
  Direct3D's have their suffixes.
- EMBERWAVE_VERSION and its parts in the header, and
  emberwave_version_number() in the library, tied to the crate's version
  at compile time.
- Compile-time assertions of the threads the docs promise, in the C API
  and in the core; callbacks must not call emberwave; the calls on the
  caller's Metal, Vulkan and Direct3D objects are marked provisional.

BREAKING CHANGE: every C caller sets struct_size and the planes' size,
renames the Metal submits, and checks the new statuses.

Claude-Session: https://claude.ai/code/session_019fJGoCTvP5e7n599q3ZQ42
The review found no unsound code and no functional bug, but two flaws in
the growth scheme, which this fixes before anything ships:

- EmberwaveVulkanInstance and EmberwaveVulkanSync ended in a 32-bit member
  after 64-bit ones, so 4 bytes of padding followed. A member added there
  would leave sizeof, and so struct_size, as it was, and the library would
  read it from an older caller's padding. Their 32-bit members come first
  now, and no struct has padding after its last member.
- FIRST_SIZE was size_of, so the first member added would have refused
  every program built with the older header. The macro now takes the last
  member of each struct's first layout, which fixes FIRST_SIZE, and the
  last member now; it asserts at compile time that struct_size comes first
  and that nothing pads the end. A test pins the first sizes on 64-bit
  systems.

And docs that contradicted the code: the budget gives BUFFER_TOO_SMALL;
the calls that lack their graphics API give UNSUPPORTED, and the enum's
doc says which calls do; the planes' safety contracts name size; the
Vulkan device's contract names its instance; BUFFER_TOO_SMALL stores a
size only where the call can tell it. A Vulkan test covers a NULL or
short instance.

Claude-Session: https://claude.ai/code/session_019fJGoCTvP5e7n599q3ZQ42
@with-developer
with-developer merged commit a07339a into main Oct 31, 2026
7 checks passed
@with-developer
with-developer deleted the feat/152-c-abi branch October 31, 2026 00:41
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