From 68bb93d4659f1665719af2a284b672d0d80adf01 Mon Sep 17 00:00:00 2001 From: Remi Collet Date: Fri, 18 Oct 2019 15:00:46 +0200 Subject: initial work --- .gitignore | 2 + README | 6 ++ preload-redis.h | 83 ++++++++++++++++++ preload-redis.inc | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ preload-zstd.h | 9 ++ preload-zstd.inc | 89 ++++++++++++++++++++ preload.php | 8 ++ redis.php | 69 +++++++++++++++ zstd.php | 66 +++++++++++++++ 9 files changed, 578 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100644 preload-redis.h create mode 100644 preload-redis.inc create mode 100644 preload-zstd.h create mode 100644 preload-zstd.inc create mode 100644 preload.php create mode 100644 redis.php create mode 100644 zstd.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8c1341 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +test* + diff --git a/README b/README new file mode 100644 index 0000000..e635582 --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +PoC, only for documentation purpose + +Copyright (c) 2019 Remi Collet +License: CC-BY-SA +http://creativecommons.org/licenses/by-sa/4.0/ + diff --git a/preload-redis.h b/preload-redis.h new file mode 100644 index 0000000..fe3673d --- /dev/null +++ b/preload-redis.h @@ -0,0 +1,83 @@ +#define FFI_SCOPE "_REMI_REDIS_" +#define FFI_LIB "libhiredis.so.0" + +/* Copy/paste from hiredis/hiredis.h and hiredis/read.h */ + +typedef struct redisReadTask { + int type; + int elements; /* number of elements in multibulk container */ + int idx; /* index in parent (array) object */ + void *obj; /* holds user-generated value for a read task */ + struct redisReadTask *parent; /* parent task */ + void *privdata; /* user-settable arbitrary field */ +} redisReadTask; + + +typedef struct redisReplyObjectFunctions { + void *(*createString)(const redisReadTask*, char*, size_t); + void *(*createArray)(const redisReadTask*, int); + void *(*createInteger)(const redisReadTask*, long long); + void *(*createNil)(const redisReadTask*); + void (*freeObject)(void*); +} redisReplyObjectFunctions; + +typedef struct redisReader { + int err; /* Error flags, 0 when there is no error */ + char errstr[128]; /* String representation of error when applicable */ + + char *buf; /* Read buffer */ + size_t pos; /* Buffer cursor */ + size_t len; /* Buffer length */ + size_t maxbuf; /* Max length of unused buffer */ + + redisReadTask rstack[9]; + int ridx; /* Index of current read task */ + void *reply; /* Temporary reply pointer */ + + redisReplyObjectFunctions *fn; + void *privdata; +} redisReader; + +enum redisConnectionType { + REDIS_CONN_TCP, + REDIS_CONN_UNIX, +}; + +typedef struct redisReply { + int type; /* REDIS_REPLY_* */ + long long integer; /* The integer when type is REDIS_REPLY_INTEGER */ + int len; /* Length of string */ + char *str; /* Used for both REDIS_REPLY_ERROR and REDIS_REPLY_STRING */ + size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */ + struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */ +} redisReply; + +typedef struct redisContext { + int err; /* Error flags, 0 when there is no error */ + char errstr[128]; /* String representation of error when applicable */ + int fd; + int flags; + char *obuf; /* Write buffer */ + redisReader *reader; /* Protocol reader */ + + enum redisConnectionType connection_type; + struct timeval *timeout; + + struct { + char *host; + char *source_addr; + int port; + } tcp; + + struct { + char *path; + } unix_sock; + +} redisContext; + +void redisFree(redisContext *c); +redisContext *redisConnect(const char *ip, int port); +redisContext *redisConnectUnix(const char *path); +redisReply *redisCommand(redisContext *c, const char *format, ...); +void freeReplyObject(void *reply); + diff --git a/preload-redis.inc b/preload-redis.inc new file mode 100644 index 0000000..199b0fd --- /dev/null +++ b/preload-redis.inc @@ -0,0 +1,246 @@ +debug) { + vprintf($f, $a); + } + } + + /** + * Parser the header and and init the FFI Singleton + */ + private function initFFI() { + if (self::$ffi) { + return; + } + $this->log("+ %s()\n", __METHOD__); + // Try if preloaded + try { + self::$ffi = \FFI::scope("_REMI_REDIS_"); + } catch (\FFI\Exception $e) { + // Try direct load + if (PHP_SAPI === 'cli' || (int)ini_get("ffi.enable")) { + self::$ffi = \FFI::load(__DIR__ . '/preload-redis.h'); + } else { + throw $e; + } + } + if (!self::$ffi) { + throw new \RuntimeException("FFI parse fails"); + } + } + + /** + * Free redisContext memory + */ + public function cleanup() { + if (!is_null($this->conn)) { + self::$ffi->redisFree($this->conn); + $this->conn = NULL; + } + } + + /** + * Constructor + connection + */ + public function __construct($path, $port=6379, $debug=false) { + $this->debug = $debug; + $this->log("+ %s(%s, %d)\n", __METHOD__, $path, $port); + $this->initFFI(); + if ($path[0] === '/') { + $this->conn = self::$ffi->redisConnectUnix($path); + } else { + $this->conn = self::$ffi->redisConnect($path, $port); + } + if ($this->conn->err) { + $msg = ''; + for($i=0 ; $i<128 && $this->conn->errstr[$i] ; $i++) { + $msg .= $this->conn->errstr[$i]; + } + $this->cleanup(); + throw new \RuntimeException($msg); + } + } + + /** + * Destructor + */ + public function __destruct() { + $this->log("+ %s\n\n", __METHOD__); + $this->cleanup(); + } + + /** + * Parse the redisReply + */ + private function resp($rep, $free=true) { + if (is_null($rep)) { + throw new \RuntimeException('Command fails'); + } + switch ($rep->type) { + case self::REDIS_REPLY_STATUS: + case self::REDIS_REPLY_STRING: + case self::REDIS_REPLY_ERROR: + $msg = ''; + for($i=0 ; $i<$rep->len ; $i++) { + $msg .= $rep->str[$i]; + } + if ($rep->type == self::REDIS_REPLY_ERROR) { + if ($free) self::$ffi->freeReplyObject($rep); + throw new \RuntimeException($msg); + } + $ret = $msg; + break; + case self::REDIS_REPLY_ARRAY: + $ret = []; + for ($i=0 ; $i<$rep->elements ; $i++) { + $ret[] = $this->resp($rep->element[$i], false); + } + break; + case self::REDIS_REPLY_INTEGER: + $ret = $rep->integer; + break; + case self::REDIS_REPLY_NIL: + $ret = NULL; + break; + default: + if ($free) self::$ffi->freeReplyObject($rep); + throw new \RuntimeException('Unkown response type'); + } + if ($free) self::$ffi->freeReplyObject($rep); + return $ret; + } + + /** + * Unkown command + */ + public function grrr() { + $this->log("+ %s()\n", __METHOD__); + return($this->resp(self::$ffi->redisCommand($this->conn, 'GRRR'))); + } + + /** + * DEL command + */ + public function del($name) { + $this->log("+ %s(%s)\n", __METHOD__, $name); + return($this->resp(self::$ffi->redisCommand($this->conn, "DEL $name"))); + } + + /** + * SET command + */ + public function set($name, $value) { + $this->log("+ %s(%s, %s)\n", __METHOD__, $name, $value); + return($this->resp(self::$ffi->redisCommand($this->conn, "SET $name %s", (string)$value))); + } + + /** + * GET command + */ + public function get($name) { + $this->log("+ %s(%s)\n", __METHOD__, $name); + return($this->resp(self::$ffi->redisCommand($this->conn, "GET $name"))); + } + + /** + * INCR command + */ + public function incr($name) { + $this->log("+ %s(%s)\n", __METHOD__, $name); + return($this->resp(self::$ffi->redisCommand($this->conn, "INCR $name"))); + } + + /** + * DECR command + */ + public function decr($name) { + $this->log("+ %s(%s)\n", __METHOD__, $name); + return($this->resp(self::$ffi->redisCommand($this->conn, "DECR $name"))); + } + + /** + * LPUSH command + */ + public function lpush($name, $elt) { + $this->log("+ %s(%s, %s)\n", __METHOD__, $name, $elt); + return($this->resp(self::$ffi->redisCommand($this->conn, "LPUSH $name %s", (string)$elt))); + } + + /** + * RPUSH command + */ + public function rpush($name, $elt) { + $this->log("+ %s(%s, %s)\n", __METHOD__, $name, $elt); + return($this->resp(self::$ffi->redisCommand($this->conn, "RPUSH $name %s", (string)$elt))); + } + + /** + * LSET command + */ + public function lset($name, $ind, $elt) { + $this->log("+ %s(%s, %d, %s)\n", __METHOD__, $name, $ind, $elt); + return($this->resp(self::$ffi->redisCommand($this->conn, "LSET $name %d %s", (int)$ind, (string)$elt))); + } + + /** + * LRANGE command + */ + public function lrange($name, $start, $end) { + $this->log("+ %s(%s, %d, %s)\n", __METHOD__, $name, $start, $end); + return($this->resp(self::$ffi->redisCommand($this->conn, "LRANGE $name %d %d", (int)$start, (int)$end))); + } + + /** + * LPOP command + */ + public function lpop($name) { + $this->log("+ %s(%s)\n", __METHOD__, $name); + return($this->resp(self::$ffi->redisCommand($this->conn, "LPOP $name"))); + } + + /** + * RPOP command + */ + public function rpop($name) { + $this->log("+ %s(%s)\n", __METHOD__, $name); + return($this->resp(self::$ffi->redisCommand($this->conn, "RPOP $name"))); + } + + /** + * LLEN command + */ + public function llen($name) { + $this->log("+ %s(%s)\n", __METHOD__, $name); + return($this->resp(self::$ffi->redisCommand($this->conn, "LLEN $name"))); + } +} + diff --git a/preload-zstd.h b/preload-zstd.h new file mode 100644 index 0000000..1dc60c0 --- /dev/null +++ b/preload-zstd.h @@ -0,0 +1,9 @@ +#define FFI_SCOPE "_REMI_ZSTD_" +#define FFI_LIB "libzstd.so.1" + +size_t ZSTD_compress(void* dst, size_t dstCapacity, const void* src, size_t srcSize, int compressionLevel); +size_t ZSTD_decompress(void* dst, size_t dstCapacity, const void* src, size_t compressedSize); +size_t ZSTD_compressBound(size_t srcSize); +unsigned long long ZSTD_decompressBound(const void* src, size_t srcSize); +unsigned ZSTD_isError(size_t code); + diff --git a/preload-zstd.inc b/preload-zstd.inc new file mode 100644 index 0000000..de8abd0 --- /dev/null +++ b/preload-zstd.inc @@ -0,0 +1,89 @@ +ZSTD_compressBound($len); + $ret['max_len'] = $max; + + $comp = str_repeat(' ', $max); + $clen = self::$ffi->ZSTD_compress($comp, $max, $src, $len, 6); + if (self::$ffi->ZSTD_isError($clen)) { + throw new \RuntimeException("Compression fails"); + } + $ret['out_len'] = $clen; + if (file_put_contents($out, substr($comp, 0, $clen)) !== $clen) { + throw new \RuntimeException("Save fails"); + } + + return $ret; + } + + public static function decompress($in, $out) { + self::init(); + + $ret = []; + $comp = file_get_contents($in); + if ($comp === false) { + throw new \RuntimeException("Read fails"); + } + $clen = strlen($comp); + $ret['in_len'] = $clen; + + $max = self::$ffi->ZSTD_decompressBound($comp, $clen); + $ret['max_len'] = $max; + + $unco = str_repeat(' ', $max); + $ulen = self::$ffi->ZSTD_decompress($unco, $max, $comp, $clen); + if (self::$ffi->ZSTD_isError($clen)) { + throw new \RuntimeException("Compression fails"); + } + $ret['out_len'] = $ulen; + if (file_put_contents($out, substr($unco, 0, $ulen)) !== $ulen) { + throw new \RuntimeException("Save fails"); + } + + return $ret; + } +} + diff --git a/preload.php b/preload.php new file mode 100644 index 0000000..1af70d6 --- /dev/null +++ b/preload.php @@ -0,0 +1,8 @@ +grrr()); +} catch(\Exception $e) { + printf("** Catched %s: %s **\n", get_class($e), $e->getMessage()); +} + +// del / set / get +var_dump($r->del('foo')); +var_dump($r->get('foo')); +var_dump($r->set('foo', date("Y/m/d H:i:s"))); +var_dump($r->get('foo')); +unset($r); + +// incr +$r = new Redis("localhost", 6379, true); +var_dump($r->set('foo', 41)); +var_dump($r->get('foo')); +var_dump($r->incr('foo')); +var_dump($r->get('foo')); + +// list +var_dump($r->rpush('mylist', 'one')); +var_dump($r->rpush('mylist', 'too')); +var_dump($r->lset('mylist', 1, 'two')); +try { + var_dump($r->lset('mylist', 9, 'nine')); +} catch(\Exception $e) { + printf("** Catched %s: %s **\n", get_class($e), $e->getMessage()); +} +var_dump($r->llen('mylist')); +var_dump($r->lrange('mylist', 0, 100)); +while($r->llen('mylist')) { + var_dump($r->lpop('mylist')); +} +var_dump($r->llen('mylist')); + +unset($r); + +// Exception in connection +try { + $r = new Redis("localhost", 1234, true); +} catch(\Exception $e) { + printf("** Catched %s: %s **\n", get_class($e), $e->getMessage()); +} + diff --git a/zstd.php b/zstd.php new file mode 100644 index 0000000..35c8876 --- /dev/null +++ b/zstd.php @@ -0,0 +1,66 @@ +