Attribute Macro tarantool::proc

source ·
#[proc]
Expand description

#[tarantool::proc] is a macro attribute for creating stored procedure functions.

#[tarantool::proc]
fn add(x: i32, y: i32) -> i32 {
    x + y
}

Create a “C” stored procedure from Tarantool and call it with arguments wrapped within a Lua table:

box.schema.func.create("libname.add", { language = 'C' })
assert(box.func['libname.add']:call({ 1, 2 }) == 3)

§Collecting stored procedures

All stored procs defined with #[tarantool::proc] attribute are automatically added to a global array and can be accessed via proc::all_procs function.

use tarantool::proc::all_procs;

#[tarantool::proc]
fn my_proc() -> i32 { 69 }

let procs = all_procs();
assert_eq!(procs[0].name(), "my_proc");

This can be used to generate stored procedure defintions for tarantool’s box.schema.func.create. Although there’s currently no easy way to fully automate this process, because of how loading dynamic modules works in tarantool. To be able to access the list of procs from a module you need to call a function defined in that module.

See how you can bootstrap proc definitions in example in examples/all_procs.

NOTE: collecting stored procedures is implemented by inserting the pointers to the functions into a static array. This can sometimes cause problems when functions marked #[tarantool::proc] are defined in the same mod as unit tests marked #[::test], specifically on MacOS. This is most likely due to a bug in the compiler, but until it is fixed we’ve added a workaround, such that procs are added to the global array under the #[cfg(not(test))] attribute. This can only affect you, if you try collecting stored procedures from the rust builtin unit tests, which there’s no reason for you to do. If you want to test collecting of stored procedures, consider using #[tarantool::test].

§Public attribute

Proc metadata returned from all_procs also contains the flag indicating if the procedure is defined with pub visibility modifier. This info may be useful to specify to which stored procedures the “public” tarantool role should have access to. Use Proc::is_public method to get this data.

Also, if for some reason you don’t want to use the visibility modifier or you want to override it (e.g. your proc is pub, but you want the flag to be false) you can specify the public attribute.

use tarantool::proc::all_procs;

#[tarantool::proc]
pub fn public_proc() {}

#[tarantool::proc]
fn private_proc() {}

#[tarantool::proc(public = true)]
fn also_public_proc() {}

#[tarantool::proc(public = false)]
pub fn also_private_proc() {}

for proc in all_procs() {
    if proc.name() == "public_proc"       { assert!( proc.is_public()) }
    if proc.name() == "private_proc"      { assert!(!proc.is_public()) }
    if proc.name() == "also_public_proc"  { assert!( proc.is_public()) }
    if proc.name() == "also_private_proc" { assert!(!proc.is_public()) }
}

§Accepting borrowed arguments

It can sometimes be more efficient to borrow the procedure’s arguments rather than copying them. This usecase is supported, however it is not entirely safe. Due to how stored procedures are implemented in tarantool, the arguments are allocated in a volatile region of memory, which can be overwritten by some tarantool operations. Therefore you cannot rely on the borrowed arguments being valid for the lifetime of the procedure call.

This proc is safe, because the data is accessed before any other calls to tarantool api:

#[tarantool::proc]
fn strlen(s: &str) -> usize {
    s.len()
}

This one however is unsafe:

use tarantool::{error::Error, index::IteratorType::Eq, space::Space};
use std::collections::HashSet;

#[tarantool::proc]
fn count_common_friends(user1: &str, user2: String) -> Result<usize, Error> {
    // A call to tarantool api.
    let space = Space::find("friends_with").unwrap();

    // This call is unsafe, because borrowed data `user1` is accessed
    // after a call to tarantool api.
    let iter = space.select(Eq, &[user1])?;
    let user1_friends: HashSet<String> = iter
        .map(|tuple| tuple.get(1).unwrap())
        .collect();

    // This call is safe, because `user2` is owned.
    let iter = space.select(Eq, &[user2])?;
    let user2_friends: HashSet<String> = iter
        .map(|tuple| tuple.get(1).unwrap())
        .collect();

    Ok(user1_friends.intersection(&user2_friends).count())
}

§Returning errors

Assuming the function’s return type is Result<T, E> (where E implements Display), the return values read as follows:

  • Ok(v): the stored procedure will return v
  • Err(e): the stored procedure will fail and e will be set as the last Tarantool error (see also TarantoolError::last)
use tarantool::{error::Error, index::IteratorType::Eq, space::Space};

#[tarantool::proc]
fn get_name(id: usize) -> Result<Option<String>, Error> {
    Ok(
        if let Some(space) = Space::find("users") {
            if let Some(row) = space.select(Eq, &[id])?.next() {
                row.get("name")
            } else {
                None
            }
        } else {
            None
        }
    )
}

§Returning custom types

The return type of the stored procedure must implement the Return trait which is implemented for most built-in types. To return an arbitrary type that implements serde::Serialize you can use the ReturnMsgpack wrapper type or the custom_ret attribute parameter.

#[derive(serde::Serialize)]
struct Complex {
    re: f64,
    im: f64,
}

#[tarantool::proc(custom_ret)]
fn sqrt(x: f64) -> Complex {
    if x < 0. {
        Complex { re: 0., im: x.abs().sqrt() }
    } else {
        Complex { re: x.sqrt(), im: 0. }
    }
}

// above is equivalent to this
use tarantool::proc::ReturnMsgpack;
#[tarantool::proc]
fn sqrt_explicit(x: f64) -> ReturnMsgpack<Complex> {
    ReturnMsgpack(
        if x < 0. {
            Complex { re: 0., im: x.abs().sqrt() }
        } else {
            Complex { re: x.sqrt(), im: 0. }
        }
    )
}

§Packed arguments

By default the stored procedure unpacks the received tuple and assigns the ith field of the tuple to the ith argument. If there are fewer arguments than there are fields in the input tuple, the unused tuple fields are ignored.

If you want to instead deserialize the tuple directly into your structure you can use the packed_args attribute parameter

#[tarantool::proc(packed_args)]
fn sum_all(vals: Vec<i32>) -> i32 {
    vals.into_iter().sum()
}

#[tarantool::proc]
fn sum_first_3(a: i32, b: i32, c: i32) -> i32 {
    a + b + c
}

In the above example sum_all will sum all the inputs values it received whereas sum_first_3 will only sum up the first 3 values

§Injecting arguments

Because the return value of the stored procedure is immediately serialized it is in theory ok to return borrowed values. Rust however will not allow you to return references to the values owned by the function. In that case you can use an injected argument, which will be created just outside the stored procedure and will be passed to it as a corresponding argument.

fn global_data() -> &'static [String] {
    todo!()
}

#[tarantool::proc]
fn get_ith<'a>(
    #[inject(global_data())]
    data: &'a [String],
    i: usize,
) -> &'a str {
    &data[i]
}

When calling the stored procedure only the actual arguments need to be specified, so in the above example get_ith will effectively have just 1 argument i. And data will be automatically injected and it’s value will be set to global_data() each time it is called.

§Debugging

There’s also a debug attribute parameter which enables debug printing of the arguments received by the stored procedure

#[tarantool::proc(debug)]
fn print_what_you_got() {}

The above stored procedure will just print any of it’s arguments to stderr and return immediately.

Create a tarantool stored procedure.

See tarantool::proc doc-comments in tarantool crate for details.