Rust で Redis module から HSCAN を呼ぶサンプル

HSCAN を呼び出す Redis module を Rust で書きます。

Redis module から HSCAN を呼んで配列を返す - umoriguのブログ の Rust 版です。

環境

  • Ubuntu 18.04
  • Redis 4.0.9
  • Rust 1.33 

ソースコード

github.com

src/lib.rs メイン処理 (fn hscan_hello_redis_command()):

extern "C" fn hscan_hello_redis_command(
    ctx: *mut RedisModuleCtx,
    argv: *mut *mut RedisModuleString,
    argc: c_int,
) -> Status {
    let args = parse_args(argv, argc).unwrap();
    if args.len() != 2 {
        return rm_wrong_arity(ctx);
    }
    let key_str = &args[1];

    rm_log(ctx, "notice", "Before call()");
    rm_reply_with_array(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
    rm_reply_with_string_buffer(ctx, key_str);
    let reply = rm_call(ctx, "HSCAN", key_str, "0");
    if rm_call_reply_type(reply) != ReplyType::Array {
        rm_log(ctx, "warning", "not array");
        return Status::Err;
    }
    rm_log(ctx, "notice", "After call()");
    let length0 = rm_call_reply_length(reply);
    if length0 != 2 {
        rm_log(ctx, "warning", "length0 is NOT 2");
        return Status::Err;
    }
    let r0 = rm_call_reply_array_element(reply, 0);
    if rm_call_reply_type(r0) == ReplyType::String {
        let mut len = 0;
        let s = rm_call_reply_string_ptr(r0, &mut len);
        match from_byte_string(s, len) {
            Ok(result) => {
                let _ = rm_reply_with_string_buffer(ctx, &result);
            }
            Err(_msg) => rm_log(ctx, "error", "from_utf8_error"),
        }
    } else {
        rm_reply_with_string_buffer(ctx, "ERR");
    }
    let r1 = rm_call_reply_array_element(reply, 1);
    let length = rm_call_reply_length(r1);

    for i in 0..length {
        let r = rm_call_reply_array_element(r1, i);
        if rm_call_reply_type(r) == ReplyType::String {
            let mut len = 0;
            let s = rm_call_reply_string_ptr(r, &mut len);
            match from_byte_string(s, len) {
                Ok(result) => {
                    let _ = rm_reply_with_string_buffer(ctx, &result);
                }
                Err(_msg) => rm_log(ctx, "error", "from_utf8_error"),
            }
        } else {
            rm_reply_with_string_buffer(ctx, "[non str]");
        }
    }
    rm_free_call_reply(reply);
    rm_log(ctx, "notice", "NOTICE!");
    rm_reply_with_long_long(ctx, length);
    rm_reply_with_string_buffer(ctx, "end");
    rm_reply_set_array_length(ctx, (length + 4) as i64);
    return Status::Ok;
} 

解説

Rustのコードから unsafeなしで呼び出せるようにRedis module API の関数を呼び出すラッパー関数を それぞれ作成する。

例:

    // int RedisModule_ReplyWithArray(RedisModuleCtx *ctx, long len);
    static RedisModule_ReplyWithArray:
        extern "C" fn(ctx: *mut RedisModuleCtx, len: c_long) -> Status;

に対して

fn rm_reply_with_array(ctx: *mut RedisModuleCtx, len: c_long) -> Status {
    unsafe { RedisModule_ReplyWithArray(ctx, len) }
}

のようなラッパー関数を定義して、Rust側で呼び出す - rm_reply_with_array(ctx);

Redis (C) 側の関数が可変長引数をとる場合、まず実際に利用する1つの引数パターンを使い関数プロトタイプを Rust で定義する。今回は RedisModule_Log()RedisModule_Call() が該当する。

    // void RedisModule_Log(RedisModuleCtx *ctx, const char *levelstr, const char *fmt, ...);
    static RedisModule_Log:
        extern "C" fn(ctx: *mut RedisModuleCtx, levelstr: *const u8, fmt: *const u8);
    // RedisModuleCallReply *RedisModule_Call(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...);
    static RedisModule_Call: extern "C" fn(
        ctx: *mut RedisModuleCtx,
        cmdname: *const u8,
        fmt: *const u8,
        arg0: *const u8,
        arg1: *const u8,
    ) -> *mut RedisModuleCallReply;

なお、可変長引数の関数を可変長引数のまま扱うには別のテクニックが必要。

redis-cell/raw.rs at master · brandur/redis-cell · GitHub では引数の数ごとに Rust module を分けて対応している ( call1, call2, call3 module )

ビルド・配置

$ cargo build
$ sudo cp target/debug/librust_hscanhello_redis_module.so /opt/
$ ls -l /opt/librust_hscanhello_redis_module.so
-rwxr-xr-x 1 root root 3783208 Oct 14 11:49 /opt/librust_hscanhello_redis_module.so

実行

$ redis-cli
127.0.0.1:6379> module load /opt/librust_hscanhello_redis_module.so
OK
127.0.0.1:6379> hscan hello 0
1) "0"
2) (empty list or set)
127.0.0.1:6379> hset hello a a1
(integer) 1
127.0.0.1:6379> hset hello b b1
(integer) 1
127.0.0.1:6379> hscan hello 0
1) "0"
2) 1) "a"
   2) "a1"
   3) "b"
   4) "b1"
127.0.0.1:6379> rusthscan hello
1) "hello"
2) "0"
3) "a"
4) "a1"
5) "b"
6) "b1"
7) (integer) 4
8) "end"
127.0.0.1:6379> 

HSCANを内部で実行するRedis moduleをRustで書くことができた。

参考

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

参考

Redis module から HSCAN を呼んで配列を返す

Redis コマンド HSCAN を使う Redis module を作ります。高レベルAPIを呼ぶRedis moduleのサンプルです。

環境

ファイル構成

  • redismodule.h
  • helloscan.c

redismodule.h は RedisModulesSDK/redismodule.h at master · RedisLabs/RedisModulesSDK · GitHub からダウンロードする。

ソースコード

hellohscan.c:

// gcc -O2 -shared -fPIC hellohscan.c -o hellohscan.so

#include "redismodule.h"
#include <stdlib.h>
#include <string.h>

int HelloHscan_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  // Check arg count
  if (argc != 2) return RedisModule_WrongArity(ctx);

  RedisModuleString* keyStr = argv[1];

  RedisModule_Log(ctx, "notice", "Before call()");
  RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
  RedisModule_ReplyWithStringBuffer(ctx, "[start]", strlen("[start]"));
  RedisModuleCallReply *reply = RedisModule_Call(ctx, "HSCAN", "sc", keyStr, "0");
  if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ARRAY) {
    RedisModule_Log(ctx, "warning", "not array");
    return REDISMODULE_ERR;
  }
  RedisModule_Log(ctx, "notice", "After call()");
  size_t length0 = RedisModule_CallReplyLength(reply);
  if (length0 != 2) {
    RedisModule_Log(ctx, "warning", "length0 is NOT 2");
    return REDISMODULE_ERR;
  }
  RedisModuleCallReply *r0 = RedisModule_CallReplyArrayElement(reply, 0);
  if (RedisModule_CallReplyType(r0) == REDISMODULE_REPLY_STRING) {
    size_t len = 0;
    const char *s = RedisModule_CallReplyStringPtr(r0, &len);
    RedisModule_ReplyWithStringBuffer(ctx, s, len);
  } else {
    RedisModule_ReplyWithStringBuffer(ctx, "ERR", strlen("ERR"));
  }
  RedisModuleCallReply *r1 = RedisModule_CallReplyArrayElement(reply, 1);
  size_t length = RedisModule_CallReplyLength(r1);

  for (size_t i = 0; i < length; i++) {
    RedisModuleCallReply *r = RedisModule_CallReplyArrayElement(r1, i);
    if (RedisModule_CallReplyType(r) == REDISMODULE_REPLY_STRING) {
      size_t len = 0;
      const char *s = RedisModule_CallReplyStringPtr(r, &len);
      RedisModule_ReplyWithStringBuffer(ctx, s, len);
    } else {
      RedisModule_ReplyWithStringBuffer(ctx, "[non str]", strlen("[non str]"));
    }
  }
  RedisModule_FreeCallReply(reply);
  RedisModule_Log(ctx, "notice", "NOTICE!");
  RedisModule_ReplyWithLongLong(ctx, length);
  RedisModule_ReplyWithStringBuffer(ctx, "[end]", strlen("[end]"));
  RedisModule_ReplySetArrayLength(ctx, length + 4);
  return REDISMODULE_OK;
}

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  if (RedisModule_Init(ctx, "hellohscan", 1, REDISMODULE_APIVER_1)
    == REDISMODULE_ERR) {
    return REDISMODULE_ERR;
  }

  if (RedisModule_CreateCommand(ctx,"hellohscan",
    HelloHscan_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
    return REDISMODULE_ERR;
  }
  return REDISMODULE_OK;
}

実行結果

$ redis-cli
127.0.0.1:6379> module load /opt/hellohscan.so
OK
127.0.0.1:6379> hscan hello 0
1) "0"
2) (empty list or set)
127.0.0.1:6379> hset hello a a1
(integer) 1
127.0.0.1:6379> hset hello b b1
(integer) 1
127.0.0.1:6379> hscan hello 0
1) "0"
2) 1) "a"
   2) "a1"
   3) "b"
   4) "b1"
127.0.0.1:6379> hellohscan hello
1) "[start]"
2) "0"
3) "a"
4) "a1"
5) "b"
6) "b1"
7) (integer) 4
8) "[end]"
127.0.0.1:6379>

解説

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  if (RedisModule_Init(ctx, "hellohscan", 1, REDISMODULE_APIVER_1)
    == REDISMODULE_ERR) {
    return REDISMODULE_ERR;
  }
  • RedisModule_OnLoad(): Redis から module load 時に呼ばれる関数。モジュール開発者が実装する
  • RedisModule_Init(): 自身のモジュール名、利用する Redis module API version、自身のバージョンを設定する
    • ここで指定したモジュール名が module listmodule unload で使われる
  • RedisModule_Init は エラー時 REDISMODULE_ERR (1) 、正常時は REDISMODULE_OK (0) が返ってくる
  • 同じように、 RedisModule_OnLoad() ではエラー時 REDISMODULE_ERR (1) 、正常時は REDISMODULE_OK (0) を返す
  if (RedisModule_CreateCommand(ctx,"hellohscan",
    HelloHscan_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
    return REDISMODULE_ERR;
  }
  return REDISMODULE_OK;
  • RedisModule_CreateCommand() で、コマンドを登録する
  • "hellohscan": 登録したいコマンド名の文字列
  • HelloHscan_RedisCommand: このファイル中で定義している、コマンド処理を書いた関数
  • "readonly": Redisのデータを書き換えないコマンドの場合は readonly を指定する
  • 問題が無ければ RedisModule_Onload() から REDISMODULE_OK を返す
int HelloHscan_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {

コマンド実装の本体。

if (argc != 2) return RedisModule_WrongArity(ctx);

引数の数が期待通りでない場合は RedisModule_WrongArity() を呼ぶ。(Redis client に引数の数が違うというエラーが伝わる)

RedisModuleString* keyStr = argv[1];

argv[0] にはコマンド名が、argv[1] 以降にはコマンドに対するパラメータが入っている。今回は field を指定しているという想定。 argvなど、RedisModule が管理する文字列は RedisModuleString が使われる。 argvについてはメモリ解放のために RedisModuleString_Free() を呼ばなくてもいい。

  RedisModule_Log(ctx, "notice", "Before call()");

Redisログファイル (例: /var/log/redis/redis-server.log ) にログを出力する。 第二引数 levelStrには "debug", "verbose", "notice", "warning" を指定可能。デフォルトで "notice" と "warning" はログ出力された。

  RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
  RedisModule_ReplyWithStringBuffer(ctx, "[start]", strlen("[start]"));
...
  RedisModule_ReplySetArrayLength(ctx, length + 4);

ReplyWithArray はコマンドの戻りが配列の時に指定する。第2引数に要素数を指定するが、要素数が決められない時は REDISMODULE_POSTPONED_ARRAY_LEN を指定し、 すべての要素を設定し終わってから ReplySetArrayLength で要素数を指定する。 ReplyWithArray の後で ReplyWithStringBuffer を呼び出すと、配列の要素の先頭から指定していることになる。 ReplyWithStringBuffer は文字列バイナリの先頭ポインタと長さで値を設定する。

  RedisModuleCallReply *reply = RedisModule_Call(ctx, "HSCAN", "sc", keyStr, "0");
...
  RedisModule_FreeCallReply(reply);

RedisModule_Call() で高レベルAPI呼び出し (Call) を行っている。Callの戻り値の型は RedisModuleCallReply* となる。 第2引数がC文字列のコマンド名 (ここでは"HSCAN")、第3引数で、第4引数以降の型を指定する。 ”sc” だと、第4引数が s: RedisModuleString* 、第5引数が c: C文字列 (NULL終端文字列) という意味になる。 結果が不要になったら最後に FreeCallReply() を呼ぶ。 戻り値の内容はコマンドによって違い、CallReplyType() でRedisとしての型を取得できる。 HSCANの場合、要素数2の配列であり、リストの続きを表す文字列が第1要素と、実データの配列が第2要素に格納される。

配列の場合は CallReplyLength() で長さを取得でき、 CallReplyArrayElement() で配列の要素を取り出す。

インデックス 内容
0 カーソル REDISMODULE_REPLY_STRING
1 内容の配列 REDISMODULE_REPLY_ARRAY

カーソルが "0" の場合、最後まで走査できたことを示す。 "0" 以外の場合、HSCANの次回呼び出し時にカーソルを指定すると続きを取得できる。

  RedisModuleCallReply *r1 = RedisModule_CallReplyArrayElement(reply, 1);
  size_t length = RedisModule_CallReplyLength(r1);

  for (size_t i = 0; i < length; i++) {
    RedisModuleCallReply *r = RedisModule_CallReplyArrayElement(r1, i);
    if (RedisModule_CallReplyType(r) == REDISMODULE_REPLY_STRING) {
      size_t len = 0;
      const char *s = RedisModule_CallReplyStringPtr(r, &len);
      RedisModule_ReplyWithStringBuffer(ctx, s, len);

callReplyからの文字列の取得は const char *s = RedisModule_CallReplyStringPtr(r0, &len); で行う。 第2引数に与えたsize_t 変数に長さが格納され、文字列の先頭ポインタが返ってくる。Null終端されていないので そのままC文字列としては扱えない。

ここでは RedisModule_ReplyWithStringBuffer(ctx, s, len); として、取得した文字列をそのままコマンドの返却文字列として設定している。

ビルド

gcc -O2 -shared -fPIC hellohscan.c -o hellohscan.so

gccはインストール済みであること。 helloscan.so を適当な場所において redis から読み取らせる。 ( /tmp 直下は Redis から読み取れなかったので /opt に配置した )

実行

$ redis-cli
127.0.0.1:6379> module load /opt/hellohscan.so
...
127.0.0.1:6379> hellohscan hello
...
127.0.0.1:6379> module unload hellohscan

redis-cli を実行、 module load (helloscan.so のパス) でモジュールをロードする。 RedisModule_CreateCommand で定義したコマンド名・引数で実行する。 module unload (モジュール名) でアンロードする。

HSCANを内部で実行するRedis moduleを作ることができた。

AWS Lambda で Dagger 2 を使う

AWS Lambda + API Gateway で Serverless Application を構築する際に Dagger 2 を使うサンプルです。

AWS Lambda 関数を使用する際のベストプラクティス - AWS Lambda において、Java では Dagger の利用が推奨されています。

依存関係の複雑さを最小限に抑えます。フレームワークを単純化して、実行コンテキスト起動時のロードの高速化を優先します。たとえば、Spring Framework などの複雑なフレームワークよりも、Dagger や Guice などの単純な Java 依存関係インジェクション (IoC) フレームワークを使用します。

アプリケーション構成

  • ApiHandler: Lambda のエントリポイント
  • Controller: ServerlessInput / ServerlessOutput を操作する
  • Service: 実際の処理を行う

ライブラリ

  • Dagger 2.14

Dagger 系クラスの説明

  • アプリケーション全体でインスタンスを共有するための AppComponent, AppModule, (@Singletone annotation)
  • HTTPリクエスト単位でインスタンスを生成・共有するための RequestComponent ,RequestModule, @Request annotation
  • RequestComponentAppComponent の Subcomponent とし、リクエストを処理するタイミングで生成する (appComponent.newRequest())
  • RequestComponent の生成時には、そのリクエストに紐づいた情報をRequestModule を使って各オブジェクトに渡すようにする (RequestModule のコンストラクタに設定する)
    • (TODO Binding Instances が使えるかもしれない。ただし型ではなく名前ベースになる)

Dagger 関連ソースコード

di/AppComponent.java

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
  RequestComponent newRequest(RequestModule requestModule);
}

di/AppModule.java

@Module
public class AppModule {
  @Provides
  @Singleton
  public static AuthModel provideAuthModel() {
    return new AuthModel();
  }
}

di/RequestScope.java

@Scope
@Retention(RetentionPolicy.RUNTIME)
@interface RequestScope {}

di/RequestComponent.java

@RequestScope
@Subcomponent(modules = {RequestModule.class})
public interface RequestComponent {
  SystemController newSystemController();

  UserController newUserController();
}

di/RequestModule.java

@Module
public class RequestModule {
  private final String serverlessInput;
  private final Object context;

  public RequestModule(String serverlessInput, Object context) {
    this.serverlessInput = serverlessInput;
    this.context = context;
  }

  @Provides
  public SystemService provideSystemService() {
    return new SystemService();
  }

  @Provides
  @RequestScope
  public UserInfo provideUserInfo() {
    return new UserInfo();
  }

  @Provides
  @Named("ServerlessInput")
  public String provideServerlessInput() {
    return serverlessInput;
  }

  @Provides
  @Named("Context")
  public Object provideContext() {
    return context;
  }
}

アプリケーションソースコード

build.gradle

plugins {
    id 'java-library'
    id "net.ltgt.apt" version "0.10"
    id 'eclipse'
}

repositories {
    jcenter()
}

dependencies {
    api 'org.apache.commons:commons-math3:3.6.1'

    testCompile 'org.jmockit:jmockit:1.42'
    testCompile 'org.junit.jupiter:junit-jupiter-api:5.2.0'
    implementation 'com.google.guava:guava:23.0'
    implementation 'org.apache.httpcomponents:httpclient:4.5.6'

    testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
    testRuntime "org.junit.platform:junit-platform-launcher:1.2.0"
    compile 'com.google.dagger:dagger:2.17'
    apt 'com.google.dagger:dagger-compiler:2.17'
}

app/ApiHandler.java

public class ApiHandler {
  private final AppComponent appComponent;

  public static void main(String[] args) {
    ApiHandler handler = new ApiHandler();
    String result = handler.handleRequest("version", "context");
    System.out.println(result);
    String result2 = handler.handleRequest("version", "context");
    System.out.println(result2);
    String result3 = handler.handleRequest("user/aaa", "context");
    System.out.println(result3);
  }

  public ApiHandler() {
    System.out.println("Create handler");
    appComponent = DaggerAppComponent.create();
  }

  public String handleRequest(String input, String context) {
    System.out.println("---start ApiHandler#handleRequest()---");
    RequestModule requestModule = new RequestModule(input, context);
    RequestComponent requestComponent = appComponent.newRequest(requestModule);
    if (input.startsWith("user")) {
      UserController uc = requestComponent.newUserController();
      return uc.getUser();

app/UserController.java

public class UserController {
  private final UserService userService;
  private final UserInfo userInfo;
  private final String serverlessInput;
  private final Object context;

  @Inject
  public UserController(
      UserService userService,
      UserInfo userInfo,
      @Named("ServerlessInput") String serverlessInput,
      @Named("Context") Object context) {
    System.out.println("Create SystemController");
    this.userService = userService;
    this.userInfo = userInfo;
    this.serverlessInput = serverlessInput;
    this.context = context;
  }

PukiWikiにTwitterアカウントでログイン

PukiWiki 1.5.1において、Twitterアカウントでログインした人にだけ編集を許可する設定のメモです。

まとめ

OpauthはTwitter以外のSNSにも対応していますが、ここではTwitterに限定した設定を行います。

確認環境

Twitterアプリ連携設定

Twitter連携アプリとして対象PukiWikiサイトを登録

https://apps.twitter.com/ へ行き Create New App ボタンで新規アプリを作成・登録します。

  • Name: PukiWikiサイトの名称
  • Description: サイトの説明
  • Website: サイトのURL
  • Callback URL: (空のまま)

Permission設定

Permissions ページ で Access - Read only に設定します。

連携に必要な情報を確認

Keys and Access Tokens ページ で “Consumer Key” と “Consumer Secret” を確認します。この値を後で opauth_config.php へ設定することになります。

PukiWiki設置サーバーでOpauth設定

opauthをcomposerでインストールします。

opauth と opauth/twitter (プラグイン) をインストール

PukiWikiindex.phpがある場所と同じディレクトリに以下のような composer.json を用意します。

{
  "require":{
    "opauth/opauth": "*",
    "opauth/twitter": "*"
  }
}

composerでopauthをインストールします。

$ composer install

twitterlogin.php を配置

以下のtwitterlogin.phpをそのまま、PukiWikiindex.phpと同じ場所に保存します。

<?php
$root_dir = __DIR__;
require $root_dir . '/opauth_config.php';

$url_after_login = filter_input(INPUT_GET, 'url_after_login');
$opauth_step2 = filter_input(INPUT_GET, '_opauth_step2');
$opauth_step3 = filter_input(INPUT_GET, '_opauth_step3');
$request_uri = filter_input(INPUT_SERVER, 'REQUEST_URI');

if ($opauth_step3 === '3') {
  // Step 3
  session_start();

  if ($url_after_login) {
    if (substr($url_after_login, 0, 1) === '/') {
      header('HTTP/1.0 302 Found');
      header("Location: " . $url_after_login);
    } elseif (substr($url_after_login, 0, strlen($redirect_url_secure_prefix))
        === $redirect_url_secure_prefix) {
      header('HTTP/1.0 302 Found');
      header("Location: " . $url_after_login);
    }
  }
  $b = isset($_SESSION['opauth']);
  if ($b) {
    $nickname = $_SESSION['opauth']['auth']['info']['nickname'];
    $_SESSION['authenticated_user'] = $nickname;
  }

?>
<html><body><pre>
Step 3

uid: <?php echo ($b ? htmlspecialchars($_SESSION['opauth']['auth']['uid']) : '') ?> .
nickname: <?php echo ($b ? htmlspecialchars($_SESSION['opauth']['auth']['info']['nickname']) : '') ?> .
name: <?php echo ($b ? htmlspecialchars($_SESSION['opauth']['auth']['info']['name']) : '') ?> .
provider: <?php echo ($b ? htmlspecialchars($_SESSION['opauth']['auth']['provider']) : '') ?> .

url_after_login: <?php echo htmlspecialchars($url_after_login) ?> .
redirect_url_secure_prefix: <?php echo htmlspecialchars($redirect_url_secure_prefix) ?> .
</pre></body></html>
<?php
    exit;
} elseif ($opauth_step2 === '2') {
  // Step 2
  require $root_dir . '/vendor/autoload.php';
  $m = [];
  if (preg_match('#/.+[\?&]_opauth_step2=2&/#', $request_uri, $m) === 1) {
    $config['path'] = $m[0];
    $config['callback_url'] = $callback_url_php
      . '?url_after_login=' . rawurlencode($url_after_login)
      . '&_opauth_step3=3';
    new Opauth($config);
  } else {
    echo "Error on step2";
  }
} else {
  // Step 1
  if (strpos($request_uri, '?') === false) {
    $opauth_path = $request_uri . '?' . '_opauth_step2=2&/';
  } else {
    $opauth_path = $request_uri . '&' . '_opauth_step2=2&/';
  }
  require $root_dir . '/vendor/autoload.php';
  $config['path'] = $opauth_path;
  $config['request_uri'] = $opauth_path  . 'twitter';
  new Opauth($config);
}

opauth_config.php の設定

<?php
$config = [
    'security_salt' => 'LDFmiilYf8Fyw5W1', // ★必ず変更する
    'Strategy' => [
        'Twitter' => [
            'key' => '<Your Consumer Key>', // ★設定する
            'secret' => '<Your Consumer Secret>', // ★設定する
        ],
    ]
];
// URL of twitterlogin.php
$callback_url_php = 'http://pukiwiki-twitter.example.com/wiki/twitterlogin.php';
$redirect_url_secure_prefix = 'http://pukiwiki-twitter.example.com/';

必ず 'security_salt' の値を変更します。 (参照: Opauth configuration · opauth/opauth Wiki · GitHub - security_salt

)

<Your Consumer Key>, <Your Consumer Secret> の部分にTwitterのApplication Management - Keys and Access Tokens で取得した値を設定します。

$callback_url_php には作成した twitterlogin.php に対応するURLを、$redirect_url_secure_prefix にはサイトのドメイン部分を設定します。 http/https部分も一致している必要があります。

Twitterログイン動作テスト

opauth_config.php$callback_url_php として設定したURL ( http://pukiwiki-twitter.example.com/wiki/twitterlogin.php ) にアクセスします。

Twitterのアプリ連携画面が表示されたら最初の段階は成功です。

f:id:umorigu:20170507055659p:plain

「連携アプリを認証」をクリックすると、次のような画面が表示されます。

f:id:umorigu:20170507055559p:plain

アプリ側でtwitterから認証情報が取れたことを確認します。

PukiWiki連携設定

PukiWiki 1.5.1 での連携設定です。

pukiwiki.ini.php の設定

pukiwiki.ini.php で外部認証の設定をします。

$scriptPukiWikiトップページを示すURLを設定します。

// Specify PukiWiki URL (default: auto)
//$script = 'http://example.com/pukiwiki/';
$script = 'https://pukiwiki-twitter.example.com/wiki/';

$auth_typeAUTH_TYPE_EXTERNAL$auth_external_login_url_base'./twitterlogin.php' を設定します。

// Authentication type
// AUTH_TYPE_NONE, AUTH_TYPE_FORM, AUTH_TYPE_BASIC, AUTH_TYPE_EXTERNAL, ...
$auth_type = AUTH_TYPE_EXTERNAL;
$auth_external_login_url_base = './twitterlogin.php';

$auth_provider_user_prefix_external には 'twitter' を設定します。

//$auth_provider_user_prefix_external = 'external:';
$auth_provider_user_prefix_external = 'twitter:';

PukiWiki連携動作テスト

PukiWiki にアクセスすると ヘッダに ログイン のリンクが表示されています。

f:id:umorigu:20170507060557p:plain

ログインをクリックして、Twitter認証画面に飛び、元の画面に戻ってくることが確認できればOKです。

ログイン後は「ログイン」のリンクが「ログアウト」に変わります。

f:id:umorigu:20170507060629p:plain

PukiWiki編集認証設定

全ページの「編集」操作にtwitterアカウントが必要な設定にします。

pukiwiki.ini.php を編集します。

$edit_auth を 1 に、$edit_auth_pages に認証な必要なページを設定します。

// Edit auth (0:Disable, 1:Enable)
$edit_auth = 1;

$edit_auth_pages = array(
        // Regex                   Username
        '#.*#'  => 'valid-user',
);

valid-user は特殊グループで、認証が通ったすべてのユーザーを表します。

PukiWiki認証設定の詳細は PukiWiki/Authentication - PukiWiki-official に記載があります。

PukiWiki編集認証動作テスト

ログアウト状態で、PukiWiki上で「編集」リンクをクリックしたとき、Twitter認証を経由してログイン状態に遷移することを確認します。

PukiWiki側でのログアウト

「ログアウト」リンクをクリックします。

Twitter側でのログアウト(アプリ連携解除)

Twitterメニューの 設定とプライバシー - アプリ連携 (https://twitter.com/settings/applications ) から、対象PukiWikiサイトの「許可を取り消す」を実行します。

ファイル配置まとめ

twitterlogin関連のファイルは、PukiWikiindex.phpと同じディレクトリに配置します。

+ pukiwiki_root/
  - wiki/
  - backup/
  - skin/
  - cache/
  - diff/
  - counter/
  - ...
  - index.php
  - pukiwiki.ini.php
  - vendor/ (composerにより追加)
  - composer.json (追加)
  - twitterlogin.php (追加)
  - opauth_config.php (追加)

リダイレクト関係図

f:id:umorigu:20170507055638p:plain

ChromeでエクスプローラからフォルダをD&Dする

WebページにフォルダをD&Dしたいと思って調べたのでメモを残しておく。


実現したいこと

  • エクスプローラからWebページにフォルダをD&Dしてアップロードしたい
  • ドロップターゲットはデザインしたい
  • Chromeで動けばよい

結論

  • フォルダのD&Dには<input type="file" webkitdirectory directory> を使う
  • ドロップターゲットはdivで好きにデザインする
  • デザインした<div>の上にtype="file"のinput要素をサイズ指定、opacity:0で配置する

試行錯誤の跡

まず、Chromeのinput type="file"にはエクスプローラからファイルをD&Dできる。すばらしいですねChrome。もうファイル選択ダイアログを開くことはない。

ここ によると webkitdirectory 属性を指定することでinput type="file"にフォルダを指定できるようだ。実際にはフォルダ内のファイルがすべて展開されて指定される。大量にファイルを含むフォルダを指定すると大変なことになるけどまぁそれはそれ。

 <input type="file" name="img" class="file" webkitdirectory directory>

次に input type="file" の見た目を変える方法を探す。ここらをみるとCSSでinputを透明にして上に重ねるのがいいらしい。前後関係を指定するのがcssのz-index。positionでrelativeやabsoluteを駆使する。よくわからないのでそのまま使わせてらおう。

でもこれだとinputのサイズを予測できない、と思ったら width, height指定が効くらしい。これで特定の領域のどこでもDropを有効にできる。どこをクリックしてもフォルダ選択ダイアログが表示される。

 <input type="file" name="img" class="file" webkitdirectory directory style="width: 100%; height: 100px">

あとはinputの下においたdivをデザインするとできあがり。D&Dした後はHTML5 のFile APIで好きに操作できる。

HTMLソースは次のようになった。

<html>
<head>
<style>
div.fileinputs {
 position: relative;
}

div.fakefile {
 position: absolute;
 top: 0px;
 left: 0px;
 z-index: 1;
 
 background-color: lightcoral;
 width: 100%;
 height: 100px;
 text-align: center;
 line-height: 100px;
 -webkit-border-radius: 10px;
}
input.file {
 position: relative;
 opacity: 0;
 z-index: 2;
 
 width: 100%;
 height: 100px;
}
</style>
</head>
<body>
<div class="fileinputs">
	<input type="file" name="img" class="file" webkitdirectory directory>
	<div class="fakefile">ここにフォルダをD&Dしてください</div>
</div>
</body>
</html>

inputのbackground-colorやopacityの値を変えると動作がよくわかる。

jQuery File Uploadも見たけどどこを変えればいいかわからなかった。