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 (GlobSet from the globset crate) for path matching
  • Line number constraints (e.g., :42 for 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:

  1. Uses ignore::WalkBuilder to walk directories while respecting .gitignore files
  2. Streams file contents via BufReader to minimize memory usage
  3. Applies fuzzy-matching algorithms in matcher.rs that 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:

  1. Lua Entry Point – The plugin/fff.lua file loads the shared library and builds the query string.
  2. FFI Crossing – Lua passes the query to the exported C function fff_search defined in crates/fff-c/src/lib.rs.
  3. Parsing Phasefff_query_parser::parse converts the raw string into a structured Query object.
  4. Search Executionsearcher/core.rs drives the directory walk, delegating line-by-line matching to matcher.rs and yielding results through the Sink trait.
  5. Result Marshaling – Matches convert from Rust Vec<Match> to C-compatible *mut fff_match_t buffers, then back to Lua tables consumed by lua/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-parser for query strings, fff-grep for file system traversal and matching, and fff-c for Lua interoperability.
  • File paths matter: crates/fff-grep/src/searcher/core.rs drives the walk, while crates/fff-grep/src/matcher.rs implements the fuzzy algorithm.
  • FFI bridge: The fff_search function in crates/fff-c/src/lib.rs exposes Rust functionality to Lua through C-compatible pointers.
  • Performance: The ignore crate and BufReader streaming 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →