Rust C ABI, C++, Go, and Native Adapter Plan
May 6, 2026 · View on GitHub
Goal
Expose ui-grid-core through a stable C ABI, layer a C++ binding over that ABI, add a Go wrapper on top of the same C layer, and then build native UI adapters for the most relevant C/C++ and Go UI stacks without duplicating grid core logic.
The shared Rust core remains the single source of truth for:
- filtering, sorting, grouping, pagination, and virtualization math
- pinning, tree, expandable, and save-state behavior
- pipeline output and view-model projection
- command handling and state transitions
The C ABI, C++, Go, and native UI adapters become transport and rendering layers only.
Non-Negotiable Architecture Rules
- No core logic duplication outside Rust.
- C is the lowest-level foreign-function boundary.
- C++ and Go bind to the C layer, not directly to Rust internals.
- Native UI adapters consume a host-neutral projected view model from Rust.
- ABI versioning is explicit and checked at runtime.
Why This Layering
The C ABI gives the widest import surface:
- C callers can bind directly.
- C++ wrappers can provide RAII, type safety, and framework adapters.
- Go can consume the same exports through
cgo. - Future languages can target the same stable ABI without rethinking the Rust boundary.
That means one canonical engine in Rust and multiple host-language adapters above it.
Proposed Crate Layout
Add these crates to the workspace in phases:
crates/ui-grid-c-abicrates/ui-grid-cppcrates/ui-grid-gocrates/ui-grid-qtcrates/ui-grid-imguicrates/ui-grid-wxwidgetscrates/ui-grid-fyne
Notes:
ui-grid-c-abiis the real boundary.ui-grid-cppis a binding layer, not a second engine.ui-grid-gois a Go package plus any thin C helpers needed forcgoergonomics.- Native UI adapter crates should stay thin and consume projected state and command APIs from Rust.
Target Frameworks
Primary C/C++ targets:
- Qt: dominant general-purpose C++ desktop/UI framework.
- Dear ImGui: high-value immediate-mode C++ target and closest conceptual bridge from
egui. - wxWidgets: broad native desktop install base and conventional retained-mode toolkit.
Primary Go target:
- Fyne: pragmatic primary Go UI target for a native Go adapter without introducing a browser shell.
Secondary / later candidates:
- FLTK
- JUCE
- Gio
- Wails
The first wave should stay focused. Qt, Dear ImGui, and Fyne are enough to validate the architecture.
Milestone 1: Freeze the Foreign Boundary
Before exporting C symbols, define what the foreign layers are allowed to see.
Deliverables
- Freeze a host-neutral engine contract around:
- grid options
- column definitions
- row payloads
- commands
- projected render state
- save/restore state
- Expand
ui-grid-contractsto carry:- ABI version
- projection/schema version
- command/event version
- Define the ownership model for foreign memory.
Required design choices
- Prefer opaque handles for engine instances.
- Prefer explicit alloc/free exports for Rust-owned strings and buffers.
- Prefer polling or snapshot retrieval over callback-heavy ABI designs initially.
- Prefer plain C structs and UTF-8 JSON/bytes over passing Rust layout types across the boundary.
Exit criteria
- The ABI contract is documented.
- Version checks are specified.
- The core team can answer “what is stable vs internal” unambiguously.
Milestone 2: Build ui-grid-c-abi
Create a cdylib / staticlib wrapper over ui-grid-core.
Library shape
Export a minimal engine lifecycle:
ui_grid_abi_version()ui_grid_engine_create()ui_grid_engine_destroy()ui_grid_engine_set_options_json()ui_grid_engine_set_rows_json()ui_grid_engine_apply_command_json()ui_grid_engine_get_projection_json()ui_grid_engine_save_state_json()ui_grid_engine_restore_state_json()ui_grid_last_error_message()ui_grid_string_free()
Projection strategy
Do not make foreign adapters rebuild grid behavior themselves.
Rust should project a host-neutral render model containing at least:
- visible columns
- visible rows / display items
- grouped rows
- pinned regions
- cell display values
- editability / focus / expansion state
- viewport and virtualization metadata
- labels / i18n strings
This projected view model is what Qt, ImGui, wxWidgets, and Fyne should render.
Why JSON first
JSON is not the final optimized transport, but it is the right first ABI transport because:
- it is easy to inspect and debug
- it keeps the first cross-language adapters simple
- it reduces FFI surface complexity while stabilizing the contract
Later optimization can add:
- packed binary snapshots
- columnar row buffers
- incremental diff payloads
Exit criteria
- C examples compile and run.
- ABI version checks work.
- A foreign host can configure the grid, request a projection, and round-trip saved state.
Milestone 3: Build the C++ Binding Layer
Add a C++ wrapper over ui-grid-c-abi.
Deliverables
- RAII engine wrapper class.
- Typed C++ DTOs or thin JSON helpers.
- Command helpers for sorting, filtering, paging, grouping, pinning, and editing.
- Projection accessors for common host-side rendering flows.
Design direction
- The wrapper owns the native handle and destroys it automatically.
- Exceptions stay out of the C ABI; map them at the C++ layer if desired.
- Keep the wrapper close to the Rust model names to preserve conceptual parity.
Exit criteria
- C++ host code can configure the engine without touching raw C strings directly.
- The wrapper is thin enough that behavior still clearly lives in Rust.
Milestone 4: Build the Go Binding Layer
Add a Go package that consumes the C ABI through cgo.
Deliverables
- Go engine wrapper with lifecycle management.
- Go-friendly
Options,ColumnDef,State, andProjectionstructs. - Helpers for row updates, commands, and state round-trips.
Design direction
- Keep the Go wrapper thin and explicit.
- Avoid hidden background threads or implicit callbacks in the first version.
- Keep ownership rules obvious across
cgo.
Exit criteria
- Go can load the Rust-backed engine and render from projected state.
- The Go package does not duplicate pipeline or state logic.
Milestone 5: Translate the egui Adapter Pattern into New Hosts
The ui-grid-egui crate is the reference native adapter. The goal is not to copy egui widget code line-for-line, but to translate its host responsibilities into equivalent adapters on top of the same Rust core.
Shared adapter principle
Each native adapter owns only:
- host widget creation
- input event capture
- scroll container plumbing
- drawing/render tree mapping
- edit widget presentation
- focus and selection presentation
Each native adapter does not own:
- sorting logic
- filtering logic
- grouping logic
- pinning math
- save-state logic
- virtualization math
Those remain in Rust core and/or the projected view-model layer.
Adapter-Specific Notes
Qt
Best fit for a retained-mode model/view adapter.
Recommended direction:
- Start with a
QAbstractScrollAreaor model/view-based integration. - Feed Rust-projected rows/columns into the Qt presentation layer.
- Route sort/filter/edit commands back through the C++ wrapper.
Dear ImGui
Best fit for the first C++ adapter after the generic binding, because the immediate-mode structure is conceptually closest to egui.
Recommended direction:
- Translate the
eguiadapter responsibilities directly into ImGui draw/layout calls. - Reuse the same projected state and command flow.
- Validate pinned-region and virtualization behavior here early.
wxWidgets
Best fit as a retained desktop adapter once the C++ wrapper is stable.
Recommended direction:
- Start from a scrollable composite widget.
- Keep data projection in Rust and only paint/layout in wxWidgets.
Fyne
Best fit for the first Go-native adapter.
Recommended direction:
- Build a Go widget backed by the Rust projection.
- Translate scroll, selection, edit, and resize events into ABI commands.
- Keep the actual grid rules in Rust.
Performance Strategy
The FFI plan should not regress the existing Rust engine’s performance.
First release performance posture
- Optimize for correctness and stable boundaries first.
- Use JSON transport initially.
- Benchmark projection size, command throughput, and redraw cadence.
Second-wave optimization targets
- incremental projection diffs
- binary projection transport
- string-table reuse
- host-side row recycling hooks
- pinned-region projection slices
Rule
Do not push rendering logic down into each foreign adapter just to avoid transport cost. Optimize the Rust-to-host transport first.
Test Strategy
Core conformance
Keep the existing Rust core tests authoritative for behavior.
ABI tests
Add tests for:
- handle lifecycle
- invalid JSON / invalid command handling
- version mismatch behavior
- save/restore round-trips
- projection schema stability
Adapter conformance
Each adapter should run the same high-level scenarios:
- sorting
- filtering
- grouping
- pinning
- pagination
- tree
- expandable rows
- editing
- save/restore
Recommended First Execution Slice
Do this first, in order:
- Expand
ui-grid-contractsto define ABI and projection versions. - Create
ui-grid-c-abiascdylib+staticlib. - Export lifecycle + JSON options/rows/commands + projection/state getters.
- Add one C smoke example.
- Add one C++ wrapper around that ABI.
- Add one Dear ImGui proof-of-concept adapter or one Qt proof-of-concept adapter.
- Add the Go wrapper after the C ABI is stable.
That sequencing keeps the lowest-level contract stable before multiplying adapters.
Deliverable Sequence
ui-grid-c-abipreview- C example app
ui-grid-cppwrapper preview- First native C++ adapter preview
ui-grid-gowrapper preview- Fyne preview adapter
Documentation Follow-Up
Once implementation begins, update:
docs/rust.mddocs/rust-egui.md- crate READMEs for each new FFI/adapter crate
- a feature parity matrix across egui, C ABI, C++, and Go adapters
Success Criteria
This effort succeeds if:
- Rust remains the only location for shared grid behavior.
- C, C++, and Go can all drive the same engine contract.
- Native adapters differ only in rendering/event plumbing.
- Feature additions continue to land in Rust core first and flow outward through bindings.