Skip to content

Add C-ABI iOS/Swift FFI wrapper crate (dithering only)#56

Open
davelee98 wants to merge 3 commits into
OpenDisplay:mainfrom
davelee98:feat/ios-dither-wrapper
Open

Add C-ABI iOS/Swift FFI wrapper crate (dithering only)#56
davelee98 wants to merge 3 commits into
OpenDisplay:mainfrom
davelee98:feat/ios-dither-wrapper

Conversation

@davelee98

@davelee98 davelee98 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Adds packages/rust/ios — a thin C-ABI wrapper around epaper-dithering-core so the OpenDisplay iOS app can call the same OKLab dithering as the website / Python, replacing a divergent sRGB-Euclidean Swift reimplementation. Sibling of the existing wasm crate.

Scope (deliberately minimal)

Matching + error diffusion only. Tone / gamut / exposure / saturation pre-processing is not surfaced — the app keeps its tone pass in Swift and hands us pre-processed pixels, so we run at DitherConfig defaults.

API (include/epaper_dithering.h)

  • ed_dither(...) -> int32_t — dither flat sRGB → palette indices.
  • ed_abi_version() -> uint32_t.

Design: plain C ABI (no UniFFI); caller-allocated output (length is deterministically width*height, so nothing crosses the boundary to free); indices in the caller's palette order (lines up with the app's wire packing, no remap); panics can't cross FFI (catch_unwind → status code).

Wiring

  • staticlib+cdylib+rlib crate, excluded from the workspace (like wasm) so Linux cargo build --workspace and the Python sdist vendoring (Packaging issues with pypi package #40) are unaffected.
  • build-xcframework.sh → device arm64 + simulator arm64/x86_64 XCFramework, stripped/LTO'd (~36 MB). The only Xcode-gated step.
  • .github/workflows/ios-test.yml — builds & tests the FFI on the host target (Ubuntu, no Xcode) every PR, including a byte-for-byte parity test against core::dither.

Verified locally (Rust 1.97.1, Xcode 26.6)

cargo test → 6/6; cargo clippy -- -D warnings → clean; build-xcframework.sh → valid XCFramework exporting ed_dither/ed_abi_version. Already consumed by the OpenDisplay iOS app (vendored XCFramework), where its output matches the reference byte-for-byte on real photos.

🤖 Generated with Claude Code

Adds `packages/rust/ios`, a thin C-ABI wrapper around
`epaper-dithering-core` so the OpenDisplay iOS app can call the same
OKLab dithering as the website/Python instead of its divergent
sRGB-Euclidean Swift reimplementation.

Scope is deliberately minimal — matching + error diffusion only. Tone/
gamut/exposure/saturation pre-processing is NOT surfaced: the app keeps
its own tone-compression pass in Swift for now and hands us
already-pre-processed pixels, so we run at DitherConfig defaults.

Design (locked for iOS-only):
- Plain C ABI (`ed_dither`, `ed_abi_version`), no UniFFI.
- Caller-allocated output: output length is deterministically
  width*height, so Swift allocates; nothing crosses the boundary to
  free.
- Indices returned in the caller's palette order, so the app's existing
  wire-format packing needs no remap.
- Panics can't cross FFI (catch_unwind -> ED_ERR_PANIC); errors are
  negative status codes.

Wiring:
- staticlib+cdylib+rlib crate, excluded from the workspace (like `wasm`)
  so Linux `cargo build --workspace` and the Python sdist vendoring are
  unaffected.
- build-xcframework.sh produces EpaperDithering.xcframework (device
  arm64 + simulator arm64/x86_64) — the only Xcode-gated step.
- ios-test.yml runs the FFI unit/integration tests on the HOST target
  (Ubuntu, no Xcode) on every PR; includes a test asserting byte-for-byte
  parity with calling core::dither directly.

Not built/verified locally: no Rust toolchain on the authoring machine.
CI (ios-test.yml) builds and tests the FFI; the XCFramework must be built
on a Mac with Rust + iOS targets. App-side ImageProcessor integration is
a follow-up PR in the app repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCmHoU5FHgL4Sec6EFfLpy
@davelee98

Copy link
Copy Markdown
Contributor Author

Retargeting to davelee98/epaper-dithering (origin) as intended.

@davelee98 davelee98 closed this Jul 18, 2026
@codecov

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

davelee98 and others added 2 commits July 18, 2026 17:49
Matches the idiom already used in the wasm crate. Verified locally:
cargo test (6 pass, incl. core::dither byte-parity), cargo clippy clean,
and build-xcframework.sh produces a valid XCFramework (device arm64 +
simulator arm64/x86_64) exporting ed_dither / ed_abi_version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCmHoU5FHgL4Sec6EFfLpy
The XCFramework is vendored into the app repo's git, so keep the static
archives lean: release profile now uses lto + codegen-units=1 +
strip="debuginfo", and build-xcframework.sh runs `strip -S -x` on each
archive (retaining the ed_dither/ed_abi_version globals needed for
linking). Cuts the framework from ~54MB to ~36MB.

panic stays "unwind" — the FFI relies on catch_unwind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCmHoU5FHgL4Sec6EFfLpy
@davelee98 davelee98 reopened this Jul 19, 2026
@davelee98

Copy link
Copy Markdown
Contributor Author

This is so I can re-use the Rust core in the iOS app.

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