fzf-native provides fzf matching through an Emacs dynamic module. It supports synchronous collections and persistent asynchronous command sessions.
The matcher comes from telescope-fzf-native.nvim. That project ports the fzf algorithm by junegunn to C.
When you need one score, use the scalar API. When Emacs already owns the candidate collection, use the batch API. When a command streams candidates during query changes, use the session API.
;; Example of basic usage
(fzf-native-score "Hot-Topic" "hp")
;; (41);; Example of no match
(fzf-native-score "Hot-Topic" "2")
;; (0);; Example of slab re-use
(let ((slab (fzf-native-make-default-slab)))
(fzf-native-score "Hello World" "er" slab)
(fzf-native-score "Example of slab re-use" "xu" slab))
;; (24)fzf-native-score returns (SCORE).
The module now sends match positions to fzf-native-highlight-fn instead of returning them.
See test cases for more examples.
The synchronous API supports Linux, macOS, FreeBSD, and Windows. The persistent session API supports macOS, Linux, and FreeBSD.
The package bundles x86-64 modules for Linux, FreeBSD, and Windows. It bundles x86-64 and arm64 modules for macOS.
The Linux module requires glibc 2.31 or later. The macOS modules require macOS 11 or later. The FreeBSD module requires FreeBSD 14.4 or later.
Prebuilt modules are in the bin/ directory.
For another architecture, build from source with M-x fzf-native-load-own-build-dyn.
M-x package-install fzf-native
Clone / download this repository and modify your load-path:
(add-to-list 'load-path (expand-file-name "/path/to/fzf-native/" user-emacs-directory)); Configuration that uses pre-built dynamic module.
(use-package fzf-native
:vc (:url "https://github.com/dangduc/fzf-native" :rev :newest)
:config
(fzf-native-load-dyn)); Configuration that builds dynamic module locally.
(use-package fzf-native
:straight
(:repo "dangduc/fzf-native"
:host github
:files (:defaults "*.c" "*.h" "*.txt"))
:init
(setq fzf-native-always-compile-module t)
:config
(fzf-native-load-own-build-dyn)); Configuration that uses pre-built dynamic module.
(use-package fzf-native
:straight
(:repo "dangduc/fzf-native"
:host github
:files (:defaults "bin"))
:config
(fzf-native-load-dyn))fussy uses fzf-native as one of its scoring backends. It integrates the synchronous API with Emacs completion styles. See the fussy architecture.
fzfa uses persistent sessions for command-backed completion.
One session can stream find, rg, or git ls-files output without blocking Emacs.
See the fzfa architecture.
One session handle keeps one producer until stop or producer exit. Submit every query update to the same handle.
(let* ((handle (fzf-native-async-start "find . -type f"))
(request-id (fzf-native-async-submit handle "module" 200)))
(unwind-protect
(fzf-native-async-snapshot handle request-id)
(fzf-native-async-stop handle)))fzf-native-async-submit returns a monotonic request ID.
An identical queued, running, or settled request keeps its existing ID.
A different request replaces obsolete queued work. It also asks the active matcher to stop.
fzf-native-async-status returns request metadata without candidate strings.
Use it for regular polling.
fzf-native-async-snapshot returns the same metadata and the last completed candidate list.
Use it after :snapshot-generation changes.
The :result-request-id field owns the retained list.
The :stale field is non-nil for another request or an older pool boundary.
The :state field is queued, running, complete, failed, superseded, idle, or unknown.
The progress fields describe the inspected request, not the retained result.
The producer fields report command completion or failure separately.
A matcher failure keeps the prior completed candidates and adds an :error value.
Candidate input can continue after a result completes. The session coalesces growth events and retries the same request ID.
All sessions share one lazy process-wide worker pool. Query updates do not start a new producer or create new scoring threads.
On POSIX platforms, fzf-native.el checks fzf-native-session-abi-version during module loading.
A stale module fails with a rebuild and restart instruction.
fzf-native-async-candidates remains the combined compatibility API.
New integrations need the request-aware API for exact result ownership.
The C module reads these variables through symbol-value.
Higher-level packages bind them at their native call sites.
Set the higher-level package option unless your code calls fzf-native directly.
| Variable | Default | Read point | Purpose |
|---|---|---|---|
fzf-native-case-mode | smart | Every scoring request | Select smart, ignored, or respected case. |
fzf-native-fuzzy | t | Every scoring request | Select fuzzy or exact default matching. |
fzf-native-batch-highlight | 25 | Every synchronous score call | Limit synchronous highlighting. |
fzf-native-async-highlight | 200 | Every asynchronous candidate build | Limit session-result highlighting. |
fzf-native-highlight-fn | default function | Every highlight operation | Apply match ranges to one string. |
fzf-native-max-line-length | 256 | Session start | Drop or truncate long producer records. |
fzf-native-async-cache-size | 40 | Session start | Limit whole-result cache entries. |
fzf-native-async-cache-bytes | 64 MiB | Session start | Limit whole-result cache bytes. |
fzf-native-async-batch-cache-bytes | 64 MiB | Session start | Limit stable-batch cache bytes. |
fzf-native-filter-only-min-pool | 10,000,000 | Session start or each batch call | Set the pool-size filter-only trigger. |
fzf-native-filter-only-length | nil | Every scoring request | Set the query-length filter-only trigger. |
fzf-native-filter-only-logic | or | Every scoring request | Combine enabled filter-only triggers. |
Filter-only mode uses fzf_has_match instead of full scoring for the first pass.
It is useful for a large pool or a short query.
Pool size and query length are independent triggers.
fzf-native-filter-only-logic combines enabled triggers with OR or AND.
A nil or zero threshold disables that trigger. If both thresholds are disabled, full scoring remains active.
The synchronous path returns matches in input order.
It does not add a completion-score property.
For a positive limit, the session path retains the first requested matches in producer order. It fully scores and ranks only that bounded visible window.
Complete membership is optional cache evidence. If it exceeds its byte limit, later requests use stable-batch evidence or another scan.
Full session scoring also bounds result materialization for a positive limit. It reduces each 64-batch window and merges that window into a running top K.
A nil or zero limit requests every result. Memory then grows with the number of matches.
This C predicate applies the same filter-only rules for Elisp callers. Pass the query character count and candidate pool size.
Highlighting runs inside the C module.
fzf_get_positions produces character positions, which the module merges into ranges.
No Elisp regular-expression pass is required.
mkdir build && cmake -B build -DCMAKE_C_FLAGS='-O3 -march=native' && cmake --build buildFuzz builds are test-only. They do not change the release module.
make fuzz-replayreplays the matcher and session sanitizer corpora.make fuzzruns bounded matcher and session libFuzzer campaigns.make fuzz-session-tsanreplays session seeds with TSan.make fuzz-elispruns randomized properties through a real Emacs module.make fuzz-upstreamcompares compatible match sets with an installed fzf.
Set FUZZ_SECONDS to change the bounded campaign duration.
Failures produce a deterministic seed or an artifact in the related build directory.
The session target reaches the reader, coordinator, worker pool, caches, publication, and teardown. See abi-fuzzing-plan.org for the planned process-level ABI campaign.
cd Code/emacs
brew install autoconf automake pkg-config ncurses gnutls libjpeg libgif libtiff libxpm libx11 libxt libxml2
autoreconf -isvf
./autogen.sh
./configure CFLAGS="-g -O0" LDFLAGS="-g" --with-ns
make bootstrap$ pwd
~/.emacs.d/packages/emacs_31/elpaca/repos/fzf-native
$ rm -Rf build; mkdir build && cmake -B build -DCMAKE_C_FLAGS='-O0 -g -march=native' && cmake --build build$ pwd
~/Code/emacs/src
$ lldb --local-lldbinit ./emacsRefer to https://github.com/svaante/dape?tab=readme-ov-file#c-c-and-rust—lldb-dap on how to install LLDB-dap.
$ brew install llvm
($(brew --prefix --installed llvm)/bin) # PREPEND (OSX already has a binary on $PATH) to $PATH.(use-package dape
:init
;; Enable repeat mode for more ergonomic `dape' use
(use-package repeat
:ensure nil
:config
(repeat-mode))
:config
(push
'(lldb-dap
modes (c-mode c-ts-mode c++-mode c++-ts-mode)
command "lldb-dap"
command-args ["--local-lldbinit"]
ensure dape-ensure-command
:type "lldb-dap"
:cwd "/Users/james/Code/emacs/src"
:program "/Users/james/Code/emacs/src/emacs")
dape-configs)
;; Turn on global bindings for setting breakpoints with mouse
(dape-breakpoint-global-mode)
;; Info buffers to the right
(setq dape-buffer-window-arrangement 'right)
;; Info buffers like gud (gdb-mi)
(setq dape-buffer-window-arrangement 'gud)
(setq dape-info-hide-mode-line nil)
;; Pulse source line (performance hit)
(add-hook 'dape-display-source-hook 'pulse-momentary-highlight-one-line)
;; Showing inlay hints
(setq dape-inlay-hints t)
;; Save buffers on startup, useful for interpreted languages
(add-hook 'dape-start-hook (lambda () (save-some-buffers t t)))
;; Kill compile buffer on build success
(add-hook 'dape-compile-hook 'kill-buffer)
;; Projectile users
(setq dape-cwd-fn 'projectile-project-root))
;; M-x dape in fzf-native-module.c
;; Set breakpoints with mouse.See architecture.org for the current implementation. It documents the public API, session lifecycle, request ownership, caches, worker scheduling, and failure model.
See multi-round-session-plan.org for the historical design record and verification results.
All credit for fzf.c goes to the telescope-fzf-native.nvim project. Much credit for Emacs module binding code goes to the hotfuzz project.