nvim-oxi gives Neovim plugins a Rust API without the RPC tax
:link: Rust bindings to all things Neovim
At a glance
- What is it?
- nvim-oxi is a set of Rust bindings to the Neovim editor API that link into the editor through the foreign function interface rather than talking to it over RPC channels, which is what makes callbacks on keymaps and scheduled functions possible. It ships as a workspace of five crates with per Neovim version feature flags and a test macro that runs your code inside a real Neovim process.
- Who is it for?
- nvim-oxi suits a plugin author who wants the Rust ecosystem and compile time checks for a Neovim plugin, and it suits anyone who needs a keymap callback or a scheduled function, which the RPC channel approach cannot provide.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 174 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- noib3/nvim-oxi1,138 ★ · -1
| Repository | 2026-10-19 | 2026-11-07 |
|---|---|---|
| noib3/nvim-oxi | 1.1k | 1.1k |
-1 Stars in the last 19 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
A binding library for plugin authors
nvim-oxi provides safe and idiomatic Rust bindings to the rich API exposed by the Neovim text editor, and the project states its intended audience directly: it is mostly intended for plugin authors, although nothing is stopping end users from writing their Neovim configs in Rust.
That second clause matters more than it sounds, because it describes a possible end state for the ecosystem rather than a niche. A configuration language with a compile time checked API, where a mistyped option name is a build failure, is a different proposition from a Lua configuration where a mistyped option is silently ignored until something does not work.
The library is published to crates.io as nvim-oxi, with the documentation on docs.rs, and the licence is MIT. The version is 0.6.0, the edition is 2024, and the minimum Rust version is 1.91.0. The repository is not archived, the last push was on 2026-05-17, and there are no GitHub releases, so the crates.io version is the thing to depend on.
The repository is a Cargo workspace with members crates/*, examples/* and tests, and the root package plus the build script are in the root. It also carries flake.nix and flake.lock, a .envrc file for direnv, and a rustfmt.toml, so the project is developed with a reproducible toolchain rather than with whatever the contributor happens to have installed.
Why FFI instead of RPC, and what that buys
The design decision that defines this library is explained in the readme's section on how it works, and it starts from the limitations of the traditional approach.
Writing Neovim plugins in languages other than the builtin ones, that is Vimscript or Lua, is normally done via RPC channels. The limitations that come with that approach are mostly about having to serialise and deserialise everything to MessagePack-encoded messages, and the readme says plainly that this prohibits things like attaching callbacks to keymaps or scheduling functions.
That prohibition is the whole point. A plugin written over RPC is a process talking to another process, and the editor's notion of a keymap callback, which is a function pointer inside the editor, cannot be expressed across that boundary. The same applies to a scheduled function, which the editor wants to call later from its own loop.
nvim-oxi takes a different approach: it leverages Rust's foreign function interface support to hook straight into the Neovim C code, allowing feature parity with in-process plugins while also avoiding the need for an extra IO layer. So a callback is a real Rust function that the editor calls, and the serialisation round trip disappears because there is no message passing at all.
The readme links a thread on the Neovim discourse that goes into more detail for anyone interested, and that thread is about calling Neovim internal functions with LuaJIT FFI and Rust, which is the same idea approached from the Lua side.
The trade-off is implied rather than stated, but it is the usual one: an FFI binding is fast and complete, and it is also coupled to the exact C API of a particular editor build, which is exactly why the crate has per version feature flags.
A fully typed API is the second argument
The readme asks why bother when Neovim already has Lua as a first class citizen, and gives two reasons.
The first is access to the Rust ecosystem. Lua is described as a great, minimal scripting language that can also be limiting when writing more complex plugins, whereas Rust is a fully fledged, statically typed language with a huge ecosystem of crates for serialisation, networking, IO, green threads and other things. For a plugin that needs to speak to a network API, decode a binary format or run several tasks at once, that ecosystem is the argument.
The second is that nvim-oxi provides a fully typed API, where everything from optional function fields to callback arguments is checked at compile time. The stated benefit is about workflow rather than about safety alone: it allows plugin authors to spend less time reading through the help docs and more time iterating via cargo checks.
That is a real observation about the Neovim API. The editor's API is large, and much of its documentation is prose describing optional parameters, where a function may or may not be present depending on the version or the state. In a typed binding, the optionality is encoded in the type, and the callback signatures are checked where you write them, so a mismatch is a compiler error rather than a runtime surprise in a keymap handler.
The two reasons work together. The typed API is only worth having if the language underneath can do the work, and the ecosystem is only worth having if the API is pleasant to call. A binding that was untyped would push you back to the docs, and a language without libraries would push you back to writing everything yourself.
Five crates, one proc macro layer
The workspace manifest shows how the library is put together, and the internal dependency list is a good map.
There are five internal crates. nvim-oxi-api is the API surface, nvim-oxi-types holds the types, nvim-oxi-macros is the procedural macro layer, nvim-oxi-libuv wraps the libuv event loop interface, and nvim-oxi-luajit covers the LuaJIT side, which is what lets a Rust plugin interoperate with Lua code inside the editor. The top level nvim-oxi package is the facade that re-exports them.
The third party dependencies tell you the same story. proc-macro2, quote and syn are the standard toolchain for writing Rust procedural macros, so the typed API is generated rather than hand written, which is the only way to have a binding for a large C API that stays correct. serde and serde_repr handle the serialisation of the values that cross into the editor, thiserror for the error types, libc for the raw system interface, and tokio for the async runtime that the green thread support relies on. mlua is pulled in with the luajit feature for the Lua bridge, and cargo_metadata is used by the macros to read the crate's own metadata when generating code.
The lint configuration is short and tells you about the codebase's shape. mixed_attributes_style is allowed in clippy, and unsafe_op_in_unsafe_fn is allowed in rust, which is a strong signal about what the binding layer looks like internally, since a binding to a C API is full of unsafe code and the pattern of unsafe operations inside unsafe functions is the norm there.
The rustdoc lints are set to deny for broken intra-doc links and private intra-doc links, which is a small quality measure that matters for a crate whose main product is its documentation, since the whole argument is that the docs are less necessary.
The docs.rs configuration builds with default features off and with the __docsrs, neovim-nightly, libuv, mlua and test features enabled, so the published documentation covers the widest possible API surface, including the nightly one.
Feature flags per Neovim version
The features section of the manifest is short and it is the mechanism that keeps a binding usable across editor versions.
There is a neovim-0-11 feature, a neovim-0-12 feature which enables neovim-0-11 as well, and a neovim-nightly feature which enables neovim-0-12 and therefore the whole chain. The dependency is cumulative in one direction, so a newer feature implies the older ones, which is how you get the union of the API surface for the version you target.
This is a sensible design for a binding library, and it has a cost that an adopter should know about. The API of the editor changes between versions, so the bindings have to change too, and the way that is expressed is that the generated API differs depending on the feature you enable. A plugin that uses a function added in a recent version cannot compile against an older one, which is a compile time error rather than a runtime one, and that is the point.
For a plugin author, the decision is which feature to declare in your crate's manifest, and that choice sets the minimum Neovim version your users need. Declaring the oldest feature you can live with maximises compatibility; declaring the newest gives you the newest API. The examples directory includes a set of projects, api.rs, calc.rs, libuv.rs, mechanic.rs and mlua.rs, each presumably showing a different facet of the binding, and the examples README covers how to set up a Rust crate for the purpose, where to place the compiled artefacts and how to load the finished plugin from Neovim.
That last part is the operational reality of this library. The deliverable is a compiled artefact, not a Lua file you drop into a plugin directory, and the examples README is the documentation for that.
Testing inside a real Neovim, with one sharp edge
The testing story is the part of this project that a competitor would have a hard time matching, and it comes down to one macro.
Turning on the test feature enables the nvim_oxi::test attribute, which replaces the regular test macro and allows you to test a piece of code from within a Neovim instance using Rust's testing framework. The example shows the shape of it, a test that sets a variable, reads it back and deletes it:
use nvim_oxi::api;
#[nvim_oxi::test]
fn set_get_del_var() {
api::set_var("foo", 42).unwrap();
assert_eq!(Ok(42), api::get_var("foo"));
assert_eq!(Ok(()), api::del_var("foo"));
}When cargo test is executed, the generated code spawns a new Neovim process with the nvim binary in your PATH, runs your test inside it, and exits. That is a genuinely different model from mocking the editor's API, because the test runs against the real thing, and it means your assertions cover the editor's actual behaviour rather than your model of it.
There is a gotcha, and it is the sort that wastes an afternoon if you hit it without warning: you cannot have two tests with the same name in the same crate, even if they belong to different modules. The example shows the case that does not work, two modules each defining a function called foo.
There is one more requirement, and it is specific to integration tests. All integration tests must live inside a separate cdylib crate with the following build script:
// build.rs
fn main() -> Result<(), nvim_oxi::tests::BuildError> {
nvim_oxi::tests::build()
}The cdylib requirement is not arbitrary. A plugin is loaded into the editor as a dynamic library, so a crate that is going to be loaded that way has to be built as one, and the build script drives the same process the plugin build uses.
Editorial conclusion
nvim-oxi suits a plugin author who wants the Rust ecosystem and compile time checks for a Neovim plugin, and it suits anyone who needs a keymap callback or a scheduled function, which the RPC channel approach cannot provide. It is the wrong choice if you want a plugin you can install from a package manager and configure in Lua, because the deliverable is a compiled artefact you place and load yourself, and a poor match if you need to support several Neovim versions at once, because the API surface is gated behind the neovim-0-11, neovim-0-12 and neovim-nightly features. Before adopting, check which Neovim version your users run, read the examples README for the artefact placement and loading steps, and plan for the fact that the crate is at version 0.6.0 with no tagged releases to pin against.
Frequently asked questions
What is nvim-oxi and who is it for?
It provides safe and idiomatic Rust bindings to the Neovim editor API, and is mostly intended for plugin authors, though the readme notes that nothing stops end users from writing their Neovim configuration in Rust. It is MIT licensed and published on crates.io as nvim-oxi.
Why not use RPC channels to write a Neovim plugin in Rust?
The readme explains that the RPC approach requires serialising everything to MessagePack encoded messages, which prohibits things like attaching callbacks to keymaps or scheduling functions. nvim-oxi instead uses Rust's foreign function interface to hook straight into the Neovim C code, giving feature parity with in-process plugins and avoiding an extra IO layer.
How do I test nvim-oxi code?
Enable the test feature and use the nvim_oxi::test attribute instead of the regular test macro. Running cargo test then spawns a new Neovim process using the nvim binary in your PATH, runs the test inside it and exits. Two tests in the same crate cannot share a name even in different modules, and integration tests must live in a separate cdylib crate with a build.rs calling nvim_oxi::tests::build().
Which Neovim versions does nvim-oxi support?
Through cargo features named for the editor version: neovim-0-11, neovim-0-12 which enables the previous one, and neovim-nightly which enables neovim-0-12 and therefore the whole chain. The crate is at version 0.6.0 with edition 2024 and a minimum Rust version of 1.91.0.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/noib3-nvim-oxi)Star history chart for your README
[](https://hysenlabs.com/projects/noib3-nvim-oxi)Was this useful?