In theory, using LuaJIT’s FFI it’s possible to access functions internal to Neovim, and after some discussion on Gitter I was able to get a mostly-working example. I’m posting it here for discussion purposes and maybe to inspire someone to write something cool.
Note: This code is by no means an example of the best way to do things, it’s just what I came up with after something like 30 minutes to an hour of tinkering around. I’m sure it could be made safer, better, etc.
Also, thank you to @mjlbach for help with getting these examples to compile and @bfredl for the C example and saying how this could be done/was possible.
The C Version
I’ll start out with an example using C and then move on to the Rust version. For this one, we first create a main.c
file with the following contents:
// main.c
extern int name_to_color(const unsigned char *name);
int get_magenta(void)
{
return name_to_color((unsigned char *)"Magenta");
}
I think the code is pretty self-explanatory, but just in case here’s what it’s doing: we define an external function name_to_color
which will be referencing this function from src/nvim/syntax.c
in Neovim. Then we create a function get_magenta
which calls this function with a value of "Magenta"
. This should be roughly equivalent to running :lua vim.api.nvim_get_color_by_name("Magenta")
from within Neovim once we call it using LuaJIT FFI.
Okay, so now that we have that C file, let’s compile it into a library we can load using LuaJIT’s ffi:
# This should work on Linux, but not macOS
$ gcc -o main.so main.c -shared -fPIC
# If you're using macOS, run this instead
$ gcc -o main.so main.c -shared -fPIC -Wl,-undefined -Wl,dynamic_lookup
Now that we have a main.so
file we can use, let’s create a new file main.lua
with this code inside:
-- main.lua
local ffi = require'ffi'
-- I don't think the `void` here is actually necessary; seems to work
-- without it.
ffi.cdef [[
int get_magenta(void);
]]
local lib = ffi.load('./main.so')
assert(lib.get_magenta() == 16711935)
The code here is pretty straightforward. First, we require the LuaJIT module ffi
, and then we define the C function we’ll be using from main.c
using ffi.cdef
(if you’re unfamiliar with Lua, the [[ ... ]]
syntax just denotes a multi-line string; in this case, it’s not actually necessary, but if we had multiple declarations it’d make things nicer to look at). Then, we load our main.so
that we just compiled with gcc
, and then from there we can use our C function get_magenta
and assert that the number returned for the color is correct.
To run this, open up main.lua
with Neovim and then run :luafile %
. Assuming everything is set up correctly (LuaJIT installed and Neovim compiled with support for it, which unfortunately I don’t know how to check/do off the top of my head; feel free to leave a comment though if you do), there should be no errors, meaning the assert didn’t fail and everything worked. And just like that, you’ve accessed an internal Neovim function via LuaJIT FFI and C!
The Rust Version
The Rust version of this isn’t that much more complicated, it just takes a bit more setup. First, you’ll want to create a new Rust library project by doing cargo new --lib <project-name>
. You can call the project whatever you like; for the purpose of this example, I’ll be using rust-example
as the project name. Then, you’ll want to add these lines to your Cargo.toml
:
# Cargo.toml
[package]
# ... I'm leaving out lines put here by cargo new --lib
build = "build.rs"
[lib]
crate-type = ["cdylib"]
[dependencies]
# You could probably use a slightly newer/older version, this is just
# what I used.
libc = "0.2"
Next, you’ll want to add the build.rs
file in the root of the project you just created (so in the same directory as your Cargo.toml
) with these contents:
// build.rs
fn main() {
// NOTE: Both of these might not be necessary, and there may be a better
// way of doing this. This is just what I found to work.
println!("cargo:rustc-cdylib-link-arg=-Wl,-undefined");
println!("cargo:rustc-cdylib-link-arg=-Wl,dynamic_lookup");
}
The purpose of this is just to tell the Rust linker to ignore the fact that the extern "C"
function we’ll define doesn’t exist at compile-time (since we aren’t linking to a library that defines them, they’ll just be available at runtime within the LuaJIT FFI environment in Neovim). From there, put these lines of code in src/lib.rs
:
// src/lib.rs
use std::ffi::*;
extern crate libc;
extern "C" {
fn name_to_color(x: *const libc::c_char) -> i32;
}
#[no_mangle]
pub extern fn get_magenta() -> i32 {
unsafe {
name_to_color(CString::new("Magenta").unwrap().into_raw())
}
}
I think the code here is pretty self-explanatory, so I won’t explain it in detail; we’re basically just doing the same thing as the C code from earlier, but in Rust instead of C.
Now, we can create our new main.lua
file in the root of the project (so same directory as Cargo.toml
and build.rs
):
-- main.lua
local ffi = require'ffi'
-- I don't think the `void` here is actually necessary; seems to work
-- without it.
ffi.cdef [[
int get_magenta(void);
]]
-- The exact string passed to `ffi.load` here will vary depending on
-- your operating system and what you named your Rust project.
local lib = ffi.load('./target/debug/librust_example.dylib')
assert(lib.get_magenta() == 16711935)
The only real difference here versus the previous main.lua
is the path we give to ffi.load
.
After that, you can run the lua file using the same command as from before (:luafile %
after opening the Lua file in Neovim) and if all goes well, there should be no errors!
Conclusions/Discussion
The idea of accessing Neovim functions directly like this via FFI is quite interesting to me; perhaps it could be used as an alternative to msgpack-rpc or remote plugins, at least for non-garbage-collected languages like Rust and C? It would take some work, but I wouldn’t be surprised if it were possible.
I tried for a bit to do the above with some other internal Neovim functions and the Rust version, and I ran into issues with the C declarations not being defined; it would’ve taken a fair bit of time to define the proper declarations using ffi.cdef
, so I stopped. @bfredl had a PR that generated a lot of the C declarations though, and I think with some extra work in the Neovim core like that more interesting things could be created like this. Having an explicit definition of what’s safe to use and what isn’t (or similar) would also be helpful, I would think.