eileennoonan
Hoping to use Rustler to create Elixir bindings for LanceDB - can you help/guide me?
Hi y’all -
I’m working on using Rustler to create Elixir bindings for this extremely nifty embedded multimodal AI / vector database LanceDB. Lance is massive, but I already know I could go very far with maybe a couple dozen NIFs.
I’m still pretty new to Rust/Rustler, so I have a couple very broad questions for folks.
First, what is your workflow like with Rust/Rustler? How does one go about getting rapid feedback from Rust? With Elixir I can very quickly open iex or ExUnit. In Rust I find myself in this double-bind where the function won’t compile, so I want to inspect the data, but I can’t because the function won’t compile…
Then Rustler adds another layer on top of this, where maybe the code would work in Rust, but I’m not returning the correct type for Rustler so it still won’t compile.
Second, this morning I’m just trying to wrap my head around what it would take to return a LanceDB Connection to Elixir. I believe I’ll want to use a ResourceArc for this. Am I right in thinking that I’ll need to implement an encoder/decoder for the lancedb::Connection struct?
Creating a lancedb::Connection is also an asynchronous operation - are there any resources on dealing with async Rustler?
I’m quite excited about this project because LanceDB is incredible and I think the Elixir world deserves easy access to it! I’m just very disoriented and I feel like a little advice and maybe even coaching would speed this up a ton.
Thank you!
~Eileen
Marked As Solved
eileennoonan
Houston, we have contact. I’ve now shared a persistent Lance database connection reference from Rust to the BEAM, sent it back from the BEAM to Rust, and used it to send some lance db metadata from Rust back to the BEAM. As far as I can tell, that’s a world first ![]()
After some help from the LanceDB discord, here’s where I’ve settled for managing connections for now:
use lancedb::Connection;
use once_cell::sync::OnceCell;
use rustler::{Env, ResourceArc, Term};
use std::{
result::Result::{Err, Ok},
sync::{Arc, Mutex},
};
use tokio::runtime::{Builder, Runtime};
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
fn get_runtime() -> &'static Runtime {
RUNTIME.get_or_init(|| {
Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create ElixirLanceDB runtime")
})
}
struct DbConnResource(Arc<Mutex<Connection>>);
#[rustler::nif(schedule = "DirtyCpu")]
fn connect(uri: String) -> ResourceArc<DbConnResource> {
let conn = get_runtime()
.block_on(async { lancedb::connect(&uri).execute().await })
.unwrap();
ResourceArc::new(DbConnResource(Arc::new(Mutex::new(conn))))
}
#[rustler::nif(schedule = "DirtyCpu")]
fn table_names(conn: ResourceArc<DbConnResource>) -> Vec<String> {
let conn = db_conn(conn);
return get_runtime().block_on(async { conn.table_names().execute().await.unwrap() });
}
fn db_conn(conn_resource: ResourceArc<DbConnResource>) -> Connection {
let connection;
{
connection = conn_resource.0.lock().unwrap().clone();
}
connection
}
#[allow(unused, non_local_definitions)]
fn load(env: Env, _: Term) -> bool {
rustler::resource!(DbConnResource, env);
true
}
rustler::init!("Elixir.ElixirLanceDB.Native", load = load);
As per the Lance team, the Mutex shouldn’t be a bottleneck if I’m not holding onto the lock for a significant period of time. If I just lock the mutex, clone the connection, then immediately release the mutex, it will not be a bottleneck at all and I don’t even have to mess around with a RwLock or even worry about starvation. It might be something in Lance’s design that allows this - can’t guarantee it will apply to other embedded DBs.
The other neat thing is the shared static Tokio runtime. The one_cell crate lets me instantiate one Tokio runtime on boot, which I can then re-use to make every function call sync. Lance’s Python library does something similar.
@bgoosman - it might be worth checking that get_runtime bit out for Kuzu? The current implementation is not a ton of code. I will probably end up needing to expand on it but for now it’s working.
I think this connection / Mutex / sync stuff will turn out to have been the hardest part. From here on out I’m expecting it’s just a lot of fleshing out function implementations. Most of that work will be in encoding/decoding various parameter and config structs.
After that it’s OTP/poolboy and then … the world! dun dun dunnnnn mwahaahahaaaa I’m stoked.
Thanks everyone for your help and encouragement! I’m going to try to keep future convos on the repo so as to avoid spamming this forum too much.
Also Liked
filmor
Encoders/decoders and resources are very different beasts.
A type that implements Encoder and/or Decoder will get translated when passing from the BEAM to Rust (Decoder) or from Rust to the BEAM (Encoder). Low-level this means that a new object is generated whenever passing over from one to the other (with a small set of exceptions, but that can be regarded as an implementation detail).
A resource type “stays” in Rust and only an opaque reference is passed from Rust to the BEAM.
You want resources when using types that “live” in Rust (e.g. if they manage files, network connections, etc.).
I refactored the way how resources are implemented recently due to Rust language changes. The “new” way is to implement the Resource trait combined with the resource_impl attribute macro:
#[rustler::resource_impl]
impl rustler::Resource for Connection {}
With this, explicit registration is not necessary anymore and you can just start using ResourceArc<Connection> in your NIFs. Just beware that the type has to be Sync.
LostKobrakai
Not much of help, but I’ve wanted to explore two separate topics with rustler recently and also hit the lack of explanations of what needs to be done to create resources. I’d love to know more how this is intended to work as well.
sax
Awesome! I haven’t been keeping up on implementing features, as I no longer have a business use case, but I spent some time a while ago getting an async runtime working in Rustler using tokio to manage Rust WebRTC from Elixir. I’ve kept it compiling and updating dependencies, so maybe it can be helpful to you as another example: GitHub - synchronal/specter: Headless webrtc client
I went about it in the way that it sounds like you are:
- Get a basic initialization of state that can be passed back to Elixir as a ResourceArc.
- Use the reference from Elixir to send a message back to the Rust runtime.
- Rustler functions that mutate state in the Rust runtime. In my case I have WebRTC peer connections, which when initialized get saved into a HashMap with a UUID as the key. I send the UUID back to Elixir.
- Add a NIF function that starts a
tokiotask with loop, returning a reference to channel sender back to Elixir. - Send a message from Elixir to the channel sender, receive the message in the task and handle (or log) the message.
- Get a pid from Elixir into the Rust runtime. Send a message to that pid from Rust, handled via
handle_info. - Send a message from Elixir to a
tokiotask via a channel, keeping track of the current Elixir pid, and returning immediately. When the task finishes its work, send a message from the task back to the Elixir pid.
I found that for my library, most of the tests could be written with ExUnit. There are definitely places where I would write unit tests in Rust, but I would personally let those emerge from specific issues and problems, or where individual functions become complex and difficult to implement with tests as verification. In my case, the integration with Elixir was the more important thing and so became the focus of my tests… if I could get the Rust to compile, I could write a test in Elixir.
dimitarvp
I will have something for both of you pretty soon. After more than 4 years, I have finally started reviving my SQLite Elixir<=>Rust bridge library and with the help of several people and an LLM I found the “blessed” ways to do several things – including the non-obvious gotcha when you think you are returning a binary but it turns out to be a list of integers on the Erlang / Elixir side.
Is it truly Rust-async btw? As in, you need the tokio runtime compiled in? Or is it something that returns a handle and just delegates work to a background thread? If the former, I haven’t yet made strides there but I remember somebody on the forum saying they learned how to pass Futures back to Elixir and resume / poll them later in the Rust code. I am sure we can dig it out in GitHub.
dtew
Polars (Rust) is the dataframe layer for Elixir Explorer , incorporated via rustler. So there might be useful code in Explorer to teach about using Rustler (I checked with ChatGPT that I wasn’t telling you rubbish … if that’s encouraging).







