3 个不稳定版本

0.2.1 2020年11月10日
0.2.0 2020年11月5日
0.1.0 2020年8月28日

#7 in #fiber

BSD-2-Clause

120KB
2K SLoC

Tarantool C API 绑定

Latest Version Docs badge

Tarantool API 绑定库,包含以下 Tarantool API:

  • Box:空间、索引、序列
  • Fibers:纤维属性、条件变量
  • CoIO
  • 事务
  • 互斥锁
  • 元组实用工具
  • 日志(见 https://docs.rs/log/0.4.11/log/
  • 错误处理

链接

另请参阅

注意! 该库目前正在开发中。API 可能会不稳定,直到版本 1.0 发布。

入门指南

以下说明将帮助您在本地计算机上运行项目的副本。有关部署,请查看教程末尾的部署说明。

先决条件

  • rustc 1.45.0 或更高版本(未测试其他版本)
  • tarantool 2.2

用法

将以下行添加到您的项目 Cargo.toml 中

[dependencies]
tarantool-module = "0.2"

[lib]
crate-type = ["cdylib"]

有关示例用法,请参阅 https://github.com/picodata/brod

存储过程

Tarantool 可以通过插件调用 Rust 代码,通过 FFI 从 Lua 调用,或作为存储过程。本教程只涉及第三种选项,即 Rust 存储过程。实际上,Rust 程序对于 Tarantool 总是 "C 函数",但由于历史原因,通常使用 "存储过程" 这个词。

本教程包含以下简单步骤

  1. examples/easy - 打印 "hello world";
  2. examples/harder - 解析传递的参数值;
  3. examples/hardest - 使用此库执行数据库管理系统插入;
  4. examples/read - 使用此库执行数据库管理系统选择;
  5. examples/write - 使用此库执行数据库管理系统替换。

通过遵循说明并查看结果,用户应该能够自信地编写自己的存储过程。

准备

确保计算机上存在以下项目

  • Tarantool 2.2
  • 一个Rustc编译器 + Cargo构建器。任何现代版本都应该可以工作

创建Cargo项目

$ cargo init --lib

将以下行添加到 Cargo.toml

[package]
name = "easy"
version = "0.1.0"
edition = "2018"
# author, license, etc

[dependencies]
tarantool-module = "0.2.0" # (1)
serde = "1.0" # (2)

[lib]
crate-type = ["cdylib"] # (3)
  1. 将库依赖项添加为 tarantool-module
  2. 添加库依赖项 Serde,这是可选的,如果您想使用Rust结构作为元组值则必需(见 此示例);
  3. 您需要编译动态库。

请求将通过Tarantool作为客户端进行。启动Tarantool,并输入以下请求

box.cfg{listen=3306}
box.schema.space.create('capi_test')
box.space.capi_test:create_index('primary')
net_box = require('net.box')
capi_connection = net_box:new(3306)

用通俗易懂的话说:创建一个名为 capi_test 的空间,并建立一个名为 capi_connection 的连接到自身。

让客户端继续运行。它将被用于稍后输入更多请求。

简单

编辑 lib.rs 文件并添加以下行

use std::os::raw::c_int;
use tarantool_module::tuple::{FunctionArgs, FunctionCtx};

#[no_mangle]
pub extern "C" fn easy(_: FunctionCtx, _: FunctionArgs) -> c_int {
    println!("hello world");
    0
}

#[no_mangle]
pub extern "C" fn easy2(_: FunctionCtx, _: FunctionArgs) -> c_int {
    println!("hello world -- easy2");
    0
}

编译程序

$ cargo build

在另一个shell中。更改目录(cd),使其与客户端运行的目录相同。将编译好的库(位于项目源文件夹子目录 target/debug 中)复制到当前文件夹,并将其重命名为 easy.so

现在回到客户端并执行以下请求

box.schema.func.create('easy', {language = 'C'})
box.schema.user.grant('guest', 'execute', 'function', 'easy')
capi_connection:call('easy')

如果这些请求看起来不熟悉,请阅读 box.schema.func.create()box.schema.user.grant()conn:call() 的描述。

重要的函数是 capi_connection:call('easy')

它的第一个任务是查找 'easy' 函数,这应该很简单,因为默认情况下,Tarantool会在当前目录下查找名为 easy.so 的文件。

它的第二个任务是调用 'easy' 函数。由于 easy() 函数在 lib.rs 中的开始部分是 println!("hello world"),屏幕上会显示 "hello world" 字样。

它的第三个任务是检查调用是否成功。由于 easy() 函数在 lib.rs 中的结尾是 return 0,没有错误信息显示,请求结束。

结果应该如下所示

tarantool> capi_connection:call('easy')
hello world
---
- []
...

现在让我们调用 lib.rs 中的另一个函数 - easy2()。这与 easy() 函数几乎相同,但有一个细节:当文件名与函数名不同时,我们必须指定 {file-name}.{function-name}

box.schema.func.create('easy.easy2', {language = 'C'})
box.schema.user.grant('guest', 'execute', 'function', 'easy.easy2')
capi_connection:call('easy.easy2')

...这次的结果将是 hello world -- easy2

结论:调用Rust函数很简单。

更难一些

创建一个名为 "harder" 的新crate。将这些行放入 lib.rs

use serde::{Deserialize, Serialize};
use std::os::raw::c_int;
use tarantool_module::tuple::{AsTuple, FunctionArgs, FunctionCtx, Tuple};

#[derive(Serialize, Deserialize)]
struct Args {
    pub fields: Vec<i32>,
}

impl AsTuple for Args {}

#[no_mangle]
pub extern "C" fn harder(_: FunctionCtx, args: FunctionArgs) -> c_int {
    let args: Tuple = args.into(); // (1)
    let args = args.into_struct::<Args>().unwrap(); // (2)
    println!("field_count = {}", args.fields.len());

    for val in args.fields {
        println!("val={}", val);
    }

    0
}
  1. 从特殊结构 FunctionArgs 中提取元组
  2. 将元组反序列化为Rust结构

编译程序,生成一个名为 harder.so 的库文件。

现在回到客户端并执行以下请求

box.schema.func.create('harder', {language = 'C'})
box.schema.user.grant('guest', 'execute', 'function', 'harder')
passable_table = {}
table.insert(passable_table, 1)
table.insert(passable_table, 2)
table.insert(passable_table, 3)
capi_connection:call('harder', passable_table)

这次调用传递了一个Lua表(passable_table)给harder()函数。该函数可以看到它,它在args参数中。

现在屏幕看起来像这样

tarantool> capi_connection:call('harder', passable_table)
field_count = 3
val=1
val=2
val=3
---
- []
...

结论:解码传递给Rust函数的参数值最初并不容易,但有一些常规可以完成这项工作。

最难的部分

创建一个新的crate "hardest"。将这些行放入lib.rs

use std::os::raw::c_int;

use serde::{Deserialize, Serialize};

use tarantool_module::space::Space;
use tarantool_module::tuple::{AsTuple, FunctionArgs, FunctionCtx};

#[derive(Serialize, Deserialize)]
struct Row {
    pub int_field: i32,
    pub str_field: String,
}

impl AsTuple for Row {}

#[no_mangle]
pub extern "C" fn hardest(ctx: FunctionCtx, _: FunctionArgs) -> c_int {
    let mut space = Space::find("capi_test").unwrap(); // (1)
    let result = space.insert( // (3)
        &Row { // (2)
            int_field: 10000,
            str_field: "String 2".to_string(),
        }
    );
    ctx.return_tuple(result.unwrap().unwrap()).unwrap()
}

这次Rust函数做了三件事

  1. 通过调用Space::find_by_name()方法找到capi_test空间;
  2. 行结构可以直接传递,它将被自动序列化为元组;
  3. 使用.insert()插入元组。

编译程序,生成一个名为hardest.so的库文件。

现在回到客户端并执行以下请求

box.schema.func.create('hardest', {language = "C"})
box.schema.user.grant('guest', 'execute', 'function', 'hardest')
box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
capi_connection:call('hardest')

现在,仍然在客户端,执行这个请求

box.space.capi_test:select()

结果应该如下所示

tarantool> box.space.capi_test:select()
---
- - [10000, 'String 2']
...

这证明了hardest()函数成功了。

读取

创建一个新的crate "read"。将这些行放入lib.rs

use std::os::raw::c_int;

use serde::{Deserialize, Serialize};

use tarantool_module::space::Space;
use tarantool_module::tuple::{AsTuple, FunctionArgs, FunctionCtx};

#[derive(Serialize, Deserialize, Debug)]
struct Row {
    pub int_field: i32,
    pub str_field: String,
}

impl AsTuple for Row {}

#[no_mangle]
pub extern "C" fn read(_: FunctionCtx, _: FunctionArgs) -> c_int {
    let space = Space::find("capi_test").unwrap(); // (1)

    let key = 10000;
    let result = space.get(&(key,)).unwrap(); // (2, 3)
    assert!(result.is_some());

    let result = result.unwrap().into_struct::<Row>().unwrap(); // (4)
    println!("value={:?}", result);

    0
}
  1. 再次,通过调用Space::find()找到capi_test空间;
  2. 使用Rust元组字面量(序列化结构的替代方法)格式化搜索键=10000;
  3. 使用.get()获取元组;
  4. 反序列化结果。

编译程序,生成一个名为read.so的库文件。

现在回到客户端并执行以下请求

box.schema.func.create('read', {language = "C"})
box.schema.user.grant('guest', 'execute', 'function', 'read')
box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
capi_connection:call('read')

capi_connection:call('read')的结果应该看起来像这样

tarantool> capi_connection:call('read')
uint value=10000.
string value=String 2.
---
- []
...

这证明了read()函数成功了。

写入

创建一个新的crate "write"。将这些行放入lib.rs

use std::os::raw::c_int;

use tarantool_module::error::{set_error, Error, TarantoolErrorCode};
use tarantool_module::fiber::sleep;
use tarantool_module::space::Space;
use tarantool_module::transaction::start_transaction;
use tarantool_module::tuple::{FunctionArgs, FunctionCtx};

#[no_mangle]
pub extern "C" fn hardest(ctx: FunctionCtx, _: FunctionArgs) -> c_int {
    let mut space = match Space::find("capi_test").unwrap() { // (1)
        None => {
            return set_error(
                file!(),
                line!(),
                &TarantoolErrorCode::ProcC,
                "Can't find space capi_test",
            )
        }
        Some(space) => space,
    };

    let row = (1, 22); // (2)

    start_transaction(|| -> Result<(), Error> { // (3)
        space.replace(&row, false)?; // (4)
        Ok(()) // (5)
    })
    .unwrap();

    sleep(0.001);
    ctx.return_mp(&row).unwrap() // (6)
}
  1. 再次,通过调用Space::find_by_name()找到capi_test空间;
  2. 准备行值;
  3. 开始一个事务;
  4. box.space.capi_test中替换元组
  5. 结束事务
    • 如果闭包返回Ok()则提交
    • 如果Error()则回滚;
  6. 使用.return_mp()方法将整个元组返回给调用者,并让调用者显示它。

编译程序,生成一个名为write.so的库文件。

现在回到客户端并执行以下请求

box.schema.func.create('write', {language = "C"})
box.schema.user.grant('guest', 'execute', 'function', 'write')
box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
capi_connection:call('write')

capi_connection:call('write')的结果应该看起来像这样

tarantool> capi_connection:call('write')
---
- [[1, 22]]
...

这证明了 write() 函数执行成功。

结论:Rust "存储过程" 可以完全访问数据库。

清理

  • 使用 box.schema.func.drop 删除每个函数元组。
  • 使用 box.schema.capi_test:drop() 删除 capi_test 空间。
  • 删除为本次教程创建的 *.so 文件。

运行测试

要运行自动化测试,请执行

make
make test

贡献

欢迎提交拉取请求。对于重大更改,请首先打开一个问题来讨论您希望更改的内容。

请确保适当地更新测试。

版本控制

我们使用 SemVer 进行版本控制。有关可用的版本,请参阅 此存储库的标签

作者

  • Anton Melnikov
  • Dmitriy Koltsov

© 2020 Picodata.io https://github.com/picodata

许可

本项目采用 BSD 许可证 - 请参阅 LICENSE 文件以获取详细信息

依赖

~2.5MB
~56K SLoC