Skip to content

feat(fff-c): add stable C accessor functions for external FFI consumers#395

Merged
dmtrKovalenko merged 3 commits intodmtrKovalenko:mainfrom
ciolansteen:feat/fff-c-stable-accessors
Apr 20, 2026
Merged

feat(fff-c): add stable C accessor functions for external FFI consumers#395
dmtrKovalenko merged 3 commits intodmtrKovalenko:mainfrom
ciolansteen:feat/fff-c-stable-accessors

Conversation

@ciolansteen
Copy link
Copy Markdown
Contributor

Motivation

fff-c is already an editor-agnostic C library — the README mentions it can be used from any language. However, the only way external consumers (Emacs Lisp, Python ctypes, scripts) can currently access struct fields is by computing byte offsets manually. This is silently fragile: any new field shifts every subsequent offset with zero compile-time signal.

This is not theoretical. JonasThowsen/fff.el is an existing Emacs integration that uses libfff_c directly via FFI, and its hardcoded offsets are already wrong against current main:

What fff.el reads Hardcoded offset What is actually there
line_content 32 FffMatchRange* (pointer to struct!)
line_number 104 byte_offset
col 120 context_before_count

The struct grew (added file_name, git_status, match_ranges, context arrays) between when fff.el was written and today, pushing all subsequent field offsets with no signal to the caller.

Change

Adds crates/fff-c/src/accessors.rs with C-exported getter functions for every field on:

  • FffFileItemrelative_path, file_name, git_status, size, modified, frecency scores, is_binary
  • FffGrepMatch — all of the above plus line_content, line_number, col, byte_offset, match ranges, context arrays, fuzzy_score, is_definition
  • FffSearchResultcount, total_matched, total_files
  • FffGrepResultcount, total_matched, total_files_searched, total_files, filtered_file_count, next_file_offset, regex_fallback_error

cbindgen picks these up automatically — no changes to cbindgen.toml needed.

Zero impact on the Neovim integration — Lua uses ffi.cdef which parses the full header and is unaffected by new functions.

Why accessor functions instead of just repr(C) guarantees

repr(C) prevents the Rust compiler from reordering fields, but does not prevent adding new fields between existing ones. Accessor functions make the struct layout a true implementation detail — callers bind to a symbol name once and are immune to layout changes forever.

Safety

Every function has an explicit null-pointer guard (returns 0 / false / null) — no UB on null input. All functions are covered by unit tests:

running 8 tests
test accessors::tests::null_file_item_returns_null_or_zero ... ok
test accessors::tests::null_grep_match_returns_null_or_zero ... ok
test accessors::tests::null_search_result_returns_zero ... ok
test accessors::tests::null_grep_result_returns_zero_or_null ... ok
test accessors::tests::file_item_getters_return_correct_values ... ok
test accessors::tests::grep_match_getters_return_correct_values ... ok
test accessors::tests::search_result_getters_return_correct_values ... ok
test accessors::tests::grep_result_getters_return_correct_values ... ok

test result: ok. 8 passed; 0 failed; 0 ignored

Usage example (Emacs Lisp)

(define-ffi-function fff--grep-match-line-content
  "fff_grep_match_get_line_content" :pointer [:pointer] fff--library)

(ffi-get-c-string (fff--grep-match-line-content match-ptr))

A companion PR will follow at JonasThowsen/fff.el migrating from hardcoded offsets to these accessor functions.

Ionut Adrian Ciolan and others added 3 commits April 20, 2026 13:01
Tests cover:
- Null-pointer guards: every getter returns its zero-value (0 / false /
  null) when passed a null pointer — no UB, no crash
- Data correctness: FffFileItem, FffGrepMatch, FffSearchResult and
  FffGrepResult getters return exactly the values stored in the struct

All 8 tests pass clean under Rust 2024 edition (no unsafe-op warnings).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dmtrKovalenko dmtrKovalenko merged commit 4daf8bb into dmtrKovalenko:main Apr 20, 2026
40 checks passed
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.

2 participants