Rust で書く 最もシンプルな Redis Module

RustでRedis moduleを書きます。

Redis module written in Rust の実装はいくつか見つかるものの、Rust的な書き方がされていて wrapperが多く読み解きにくい。 ここでは構造を理解するために、最小限の要素だけで構成したサンプルを作る。 warningは無視し、Rustらしさは考えないことにする。

背景

Redis Moduleを作りたかったがC/C++で書くのはつらいな、もうちょっと楽したいな、と考えてRustにたどり着いた。 Rustはモダンな書き方ができ、C/C++並みの処理速度と言われている。

環境

ソースコード

github.com

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

参考