eileennoonan

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

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 :tada:

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

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

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

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 tokio task 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 tokio task 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

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

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).

FYI, Lance supports Polars

Where Next?

Popular in Questions Top

lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New

We're in Beta

About us Mission Statement