Rust で書く 最もシンプルな Redis Module
RustでRedis moduleを書きます。
Redis module written in Rust の実装はいくつか見つかるものの、Rust的な書き方がされていて wrapperが多く読み解きにくい。 ここでは構造を理解するために、最小限の要素だけで構成したサンプルを作る。 warningは無視し、Rustらしさは考えないことにする。
背景
Redis Moduleを作りたかったがC/C++で書くのはつらいな、もうちょっと楽したいな、と考えてRustにたどり着いた。 Rustはモダンな書き方ができ、C/C++並みの処理速度と言われている。
環境
- Ubuntu 18.04
- Rust 1.33.0
ソースコード
src/redismodule.c:
#include "include/redismodule.h" int Export_RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) { return RedisModule_Init(ctx, name, ver, apiver); }
src/lib.rs:
extern crate libc; use libc::{c_int, size_t}; const MODULE_NAME: &'static str = "rusthello"; const REDISMODULE_OK: c_int = 0; const REDISMODULE_ERR: c_int = 1; const REDISMODULE_APIVER_1: c_int = 1; pub enum RedisModuleCtx {} pub enum RedisModuleString {} pub type RedisModuleCmdFunc = extern "C" fn( ctx: *mut RedisModuleCtx, argv: *mut *mut RedisModuleString, argc: c_int, ) -> c_int; extern "C" { pub fn Export_RedisModule_Init( ctx: *mut RedisModuleCtx, modulename: *const u8, module_version: c_int, api_version: c_int, ) -> c_int; // int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *name, // RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, // int lastkey, int keystep); static RedisModule_CreateCommand: extern "C" fn( ctx: *mut RedisModuleCtx, name: *const u8, fmdfunc: RedisModuleCmdFunc, strflags: *const u8, firstkey: c_int, lastkey: c_int, keystep: c_int, ) -> c_int; // int RedisModule_ReplyWithStringBuffer(RedisModuleCtx *ctx, // const char *buf, size_t len); static RedisModule_ReplyWithStringBuffer: extern "C" fn(ctx: *mut RedisModuleCtx, str: *const u8, len: size_t) -> c_int; } extern "C" fn RustHello_RedisCommand( ctx: *mut RedisModuleCtx, argv: *mut *mut RedisModuleString, argc: c_int, ) -> c_int { unsafe { const HELLO: &'static str = "hello"; RedisModule_ReplyWithStringBuffer(ctx, format!("{}", HELLO).as_ptr(), HELLO.len()); } return REDISMODULE_OK; } #[no_mangle] pub extern "C" fn RedisModule_OnLoad( ctx: *mut RedisModuleCtx, argv: *mut *mut RedisModuleString, argc: c_int, ) -> c_int { unsafe { Export_RedisModule_Init( ctx, format!("{}\0", MODULE_NAME).as_ptr(), 1, REDISMODULE_APIVER_1, ); if RedisModule_CreateCommand( ctx, format!("{}\0", "rusthello").as_ptr(), RustHello_RedisCommand, format!("{}\0", "readonly").as_ptr(), 0, 0, 0, ) == REDISMODULE_ERR { return REDISMODULE_ERR; } } return REDISMODULE_OK; }
解説
redismodule.c:
#include "include/redismodule.h" int Export_RedisModule_Init(Redishttps://www.slideshare.net/poga/writing-redis-module-with-rustModuleCtx *ctx, const char *name, int ver, int apiver) { return RedisModule_Init(ctx, name, ver, apiver); }
唯一のCのコード。Rustで書かずに .c にしている理由は、 redisumodule.h (Redis側から提供されるヘッダファイル) の中で RedisModule_Init()
が定義されているため。(Redis本体側に RedisModule_Init()
という関数が存在せず、Rustから直接呼び出すことができない。 )
Cのコード中で RedisModule_Init
を呼び出すことにし、それをwrapする関数(ここでは Export_RedisModule_Init()
)をRust側から読んでいる。
参考: Writing Redis Module with Rust
src/lib.rs:
const REDISMODULE_OK: c_int = 0; const REDISMODULE_ERR: c_int = 1;
RedisModule_Init()
や RedisModule_OnLoad()
で使われる、OK/NGを表す定数。エラー時 REDISMODULE_ERR (1) 、正常時は REDISMODULE_OK (0) を返す。
pub enum RedisModuleCtx {} pub enum RedisModuleString {}
Rust FFIで、「実装の詳細が隠蔽されたCのStructのポインタ」を表すのは空のenumを使うのが定石らしい。
他言語関数インターフェイス - オペーク構造体の表現 に記載あり。
pub type RedisModuleCmdFunc = extern "C" fn( ctx: *mut RedisModuleCtx, argv: *mut *mut RedisModuleString, argc: c_int, ) -> c_int;
RedisModule_CreateCommand()
に渡すコールバック関数 ( 今回のコードの中では RustHello_RedisCommand
) の型定義。
exten "C" fn(...)
でC言語呼び出し規約に沿った関数であることを表している。
extern "C" {
ここから、Redis側 (C言語側) の関数のシグネチャ関数を定義するので C言語呼び出し規則に沿うように extern "C" { ... }
で各関数を囲む。
pub fn Export_RedisModule_Init( ctx: *mut RedisModuleCtx, modulename: *const u8, module_version: c_int, api_version: c_int, ) -> c_int;
redismodule.c で定義した ( RedisModule_Init()
のラッパー関数の ) Export_RedisModule_Init()
のプロトタイプ定義。
// int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *name, // RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, // int lastkey, int keystep); static RedisModule_CreateCommand: extern "C" fn( ctx: *mut RedisModuleCtx, name: *const u8, fmdfunc: RedisModuleCmdFunc, strflags: *const u8, firstkey: c_int, lastkey: c_int, keystep: c_int, ) -> c_int;
Redis module SDK の関数 RedisModule_CreateCommand()
のシグネチャ定義。
他言語関数インターフェイス - 他言語関数の呼出し にあるように、
C言語側 (今回は Redis Module SDK 関数) を呼び出す場合は
都度このように関数シグネチャを書いて、Rust側で「この関数はどういう形をしているのか」を教える必要がある。
コメントでC言語側のプロトタイプ宣言を参考に書いている。
// int RedisModule_ReplyWithStringBuffer(RedisModuleCtx *ctx, // const char *buf, size_t len); static RedisModule_ReplyWithStringBuffer: extern "C" fn(ctx: *mut RedisModuleCtx, str: *const u8, len: size_t) -> c_int; }
Redis module SDK の関数 RedisModule_ReplyWithStringBuffer()
のシグネチャ定義。
今回は Redi module SDK 内で呼び出す関数はこの2つのみ。
extern "C" fn RustHello_RedisCommand( ctx: *mut RedisModuleCtx, argv: *mut *mut RedisModuleString, argc: c_int, ) -> c_int { unsafe { const HELLO: &'static str = "hello"; RedisModule_ReplyWithStringBuffer(ctx, format!("{}", HELLO).as_ptr(), HELLO.len()); } return REDISMODULE_OK; }
RedisModule_CreateCommand()
に渡す、今回定義するRedisコマンド本体の実装。シンプルに ”hello” の固定文字列を Reply するだけ。
#[no_mangle] pub extern "C" fn RedisModule_OnLoad( ctx: *mut RedisModuleCtx, argv: *mut *mut RedisModuleString, argc: c_int, ) -> c_int {
Redis側から最初に呼ばれる関数 RedisModule_OnLoad()
の定義。
他言語関数インターフェイス - CからのRustのコードの呼出し にあるように、
#[no_mangle]
アトリビュートで関数名がそのままexportされるようする。
unsafe { Export_RedisModule_Init( ctx, format!("{}\0", MODULE_NAME).as_ptr(), 1, REDISMODULE_APIVER_1, ); if RedisModule_CreateCommand( ctx, format!("{}\0", "rusthello").as_ptr(), RustHello_RedisCommand, format!("{}\0", "readonly").as_ptr(), 0, 0, 0, ) == REDISMODULE_ERR { return REDISMODULE_ERR; } }
関数 RedisModule_OnLoad()
内の実装。C言語関数を呼び出すので全体を unsafe { ... }
で囲んでいる。
RedisModule_Init()
でモジュール自身の情報を登録し、 RedisModule_CreateCommand()
で
先ほど定義した RustHello_RedisCommand()
関数を "hrusthello" のコマンドとして登録している。
Build.rs:
extern crate cc; fn main() { cc::Build::new() .file("src/redismodule.c") .include("include/") .compile("libredismodule.a"); }
C言語ファイル redismodule.c をビルドしてリンクするためにccクレートを利用している。 Rust + Cの実装ではgcc クレートを使っているサンプルもあるが、gccクレートはdeprecatedになっていてccクレートを使うように促される。
Cargo.toml:
[lib] crate-type = ["dylib"] [dependencies] libc = "0.2" [build-dependencies] cc = "1.0"
ライブラリ指定 (dylib)。ccクレートを依存関係に追加。
ビルド
$ cargo build
配置
$ sudo cp target/debug/librust_simple_redis_module.so /opt/ $ ls -l /opt/librust_simple_redis_module.so -rwxr-xr-x 1 root root 3597544 Oct 8 17:27 /opt/librust_simple_redis_module.so
実行
$ redis-cli 127.0.0.1:6379> module load /opt/librust_simple_redis_module.so OK 127.0.0.1:6379> rusthello "hello" 127.0.0.1:6379> module unload rusthello OK 127.0.0.1:6379> rusthello (error) ERR unknown command 'rusthello' 127.0.0.1:6379> exit