3 个不稳定版本
0.2.1 | 2020年11月10日 |
---|---|
0.2.0 | 2020年11月5日 |
0.1.0 | 2020年8月28日 |
#7 in #fiber
120KB
2K SLoC
Tarantool C API 绑定
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 函数",但由于历史原因,通常使用 "存储过程" 这个词。
本教程包含以下简单步骤
examples/easy
- 打印 "hello world";examples/harder
- 解析传递的参数值;examples/hardest
- 使用此库执行数据库管理系统插入;examples/read
- 使用此库执行数据库管理系统选择;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)
请求将通过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
}
- 从特殊结构
FunctionArgs
中提取元组 - 将元组反序列化为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函数做了三件事
- 通过调用
Space::find_by_name()
方法找到capi_test
空间; - 行结构可以直接传递,它将被自动序列化为元组;
- 使用
.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
}
- 再次,通过调用
Space::find()
找到capi_test
空间; - 使用Rust元组字面量(序列化结构的替代方法)格式化搜索键=10000;
- 使用
.get()
获取元组; - 反序列化结果。
编译程序,生成一个名为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)
}
- 再次,通过调用
Space::find_by_name()
找到capi_test
空间; - 准备行值;
- 开始一个事务;
- 在
box.space.capi_test
中替换元组 - 结束事务
- 如果闭包返回
Ok()
则提交 - 如果
Error()
则回滚;
- 如果闭包返回
- 使用
.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