How fff.nvim's Rust Backend Works: Architecture and FFI Bridge
fff.nvim delegates file-system scanning, pattern matching, and result streaming to a native Rust library invoked via C-FFI, enabling near-instant fuzzy finding across large codebases while keeping Neovim's UI responsive.
fff.nvim is a Neovim fuzzy finder plugin that leverages a high-performance Rust backend to handle search operations. The architecture separates UI concerns in Lua from computational heavy lifting in Rust, connected through a thin C-FFI bridge. Understanding how fff.nvim's Rust backend works reveals why the plugin achieves sub-second search results even in repositories containing millions of lines of code.
The Three-Component Architecture
The Rust codebase is organized into three specialized crates that handle distinct phases of the search pipeline.
Query Parser (fff-query-parser)
The query parser transforms user input strings into structured search constraints. Located in crates/fff-query-parser/src/parser.rs and crates/fff-query-parser/src/config.rs, this component parses fff-style queries like "src/**/*.rs:42" into a Query struct containing:
- Glob patterns (
GlobSetfrom theglobsetcrate) for path matching - Line number constraints (e.g.,
:42for specific lines) - Exclusion rules (e.g.,
!node_modules/**)
Grepping Engine (fff-grep)
The grepping engine orchestrates file system traversal and pattern matching. Implemented across crates/fff-grep/src/searcher/core.rs, crates/fff-grep/src/searcher/glue.rs, and crates/fff-grep/src/matcher.rs, this component:
- Uses
ignore::WalkBuilderto walk directories while respecting.gitignorefiles - Streams file contents via
BufReaderto minimize memory usage - Applies fuzzy-matching algorithms in
matcher.rsthat toggle between "plain grep" and "fuzzy path" modes depending on the query structure
C-FFI Bridge (fff-c)
The C-FFI bridge exposes Rust functionality to Lua through a C-compatible API. Defined in crates/fff-c/src/lib.rs and crates/fff-c/include/fff.h, this layer compiles into a shared object (.so or .dylib) that Neovim loads at runtime via require('fff').
From Lua to Rust: The Execution Flow
When you execute :Fff or call fff.search(), the request traverses through four distinct layers:
- Lua Entry Point – The
plugin/fff.luafile loads the shared library and builds the query string. - FFI Crossing – Lua passes the query to the exported C function
fff_searchdefined incrates/fff-c/src/lib.rs. - Parsing Phase –
fff_query_parser::parseconverts the raw string into a structuredQueryobject. - Search Execution –
searcher/core.rsdrives the directory walk, delegating line-by-line matching tomatcher.rsand yielding results through theSinktrait. - Result Marshaling – Matches convert from Rust
Vec<Match>to C-compatible*mut fff_match_tbuffers, then back to Lua tables consumed bylua/fff/picker_ui.lua.
Key Implementation Details
File System Walking with ignore
The backend leverages the ignore crate's WalkBuilder for efficient directory traversal. Unlike naive recursion, this respects project-level ignore patterns (.gitignore, .fffignore) and utilizes parallel walking where possible. The configuration logic in crates/fff-grep/src/searcher/glue.rs wires these constraints into the walker before streaming begins.
Synchronous Callbacks with Asynchronous Wrappers
While the Rust search itself runs synchronously, fff.nvim wraps calls inside vim.loop threads to prevent UI blocking. The Lua side registers a callback (picker_ui.open_with_callback) that triggers when fff_search returns its C-compatible buffer. This architecture keeps Neovim responsive even during deep recursive searches.
Pre-Built Binary Distribution
The project ships pre-compiled binaries for Linux (packages/fff-bin-linux-x64-gnu) and macOS (packages/fff-bin-darwin-x64), eliminating the need for users to install Rust toolchains. Local compilation remains supported for custom extensions, allowing developers to modify matcher.rs or add additional file-type filters in config.rs.
Code Examples
Invoking the Rust Search from Lua
local query = "src/**/*.rs:10"
local function on_result(matches, _, location)
-- `matches` is a list of {path = "...", line = 10, text = "..."}
require('fff.picker_ui').show(matches, location)
end
require('fff').search(query, on_result)
Exported C Function in crates/fff-c/src/lib.rs
#[no_mangle]
pub extern "C" fn fff_search(
query: *const c_char,
callback: extern "C" fn(*const fff_match_t, usize),
) {
let q = unsafe { CStr::from_ptr(query) }.to_string_lossy();
let parsed = query_parser::parse(&q);
let matches = grep::search(parsed);
// Convert matches to C structs and invoke the callback
let c_matches = ffi::to_ffi(matches);
callback(c_matches.as_ptr(), c_matches.len());
}
Workspace Configuration
[workspace]
members = [
"crates/fff-c",
"crates/fff-grep",
"crates/fff-query-parser",
]
[dependencies]
ignore = "0.4"
globset = "0.4"
Summary
- fff.nvim's Rust backend consists of three crates:
fff-query-parserfor query strings,fff-grepfor file system traversal and matching, andfff-cfor Lua interoperability. - File paths matter:
crates/fff-grep/src/searcher/core.rsdrives the walk, whilecrates/fff-grep/src/matcher.rsimplements the fuzzy algorithm. - FFI bridge: The
fff_searchfunction incrates/fff-c/src/lib.rsexposes Rust functionality to Lua through C-compatible pointers. - Performance: The
ignorecrate andBufReaderstreaming enable efficient searching of large repositories without blocking the Neovim UI. - Extensibility: Pre-built binaries cover major platforms, but the workspace structure supports local Rust modifications for custom behavior.
Frequently Asked Questions
How does fff.nvim communicate between Lua and Rust?
fff.nvim uses a C Foreign Function Interface (FFI) bridge compiled into a shared object. The fff-c crate exposes functions like fff_search that accept C strings and callbacks, which Lua loads via require('fff') and invokes using Neovim's LuaJIT FFI capabilities. This allows Lua to pass query strings to Rust and receive structured match data back as Lua tables.
What Rust crates power the file system walking in fff.nvim?
The backend relies on the ignore crate for directory traversal, which provides ignore::WalkBuilder to respect .gitignore patterns and .fffignore rules. For pattern matching, it uses the globset crate to compile glob patterns into efficient GlobSet matchers that filter paths during the walk.
Is the Rust search synchronous or asynchronous?
The Rust code itself operates synchronously through the fff_search export function. However, fff.nvim wraps these calls inside vim.loop threads on the Lua side, making the overall user experience asynchronous. The UI remains responsive because picker_ui.open_with_callback handles results only after the Rust thread completes its search.
Can I modify the fuzzy matching algorithm?
Yes. Because the core logic resides in crates/fff-grep/src/matcher.rs, you can fork the repository and modify the matching implementation. After changes, recompile the workspace using cargo build --release in the crates/fff-c directory, and Neovim will load your custom shared library instead of the pre-built binary.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →