diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README | 6 | ||||
-rw-r--r-- | preload-redis.h | 83 | ||||
-rw-r--r-- | preload-redis.inc | 246 | ||||
-rw-r--r-- | preload-zstd.h | 9 | ||||
-rw-r--r-- | preload-zstd.inc | 89 | ||||
-rw-r--r-- | preload.php | 8 | ||||
-rw-r--r-- | redis.php | 69 | ||||
-rw-r--r-- | zstd.php | 66 |
9 files changed, 578 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8c1341 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +test* + @@ -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 @@ +<?php +/** + * Redis connector using FFI and libhiredis + * PoC, only for documentation purpose + * + * Copyright (c) 2019 Remi Collet + * License: CC-BY-SA + * http://creativecommons.org/licenses/by-sa/4.0/ + */ +namespace Remi; + +class Redis { + // Singleton + static private $ffi = NULL; + // Redis connection: redisContext + private $conn = NULL; + // Log method calls + private $debug = false; + + // From hiredis/read.h, REDIS_REPLY_* macros + const REDIS_REPLY_STRING = 1; + const REDIS_REPLY_ARRAY = 2; + const REDIS_REPLY_INTEGER = 3; + const REDIS_REPLY_NIL = 4; + const REDIS_REPLY_STATUS = 5; + const REDIS_REPLY_ERROR = 6; + + /** + * Display debug information + */ + private function log($f, ...$a) { + if ($this->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 @@ +<?php +/** + * ZSTD compressor using FFI and libzstd + * PoC, only for documentation purpose + * + * Copyright (c) 2019 Remi Collet + * License: CC-BY-SA + * http://creativecommons.org/licenses/by-sa/4.0/ + */ +namespace Remi; + +class Zstd { + static private $ffi = null; + + public static function init() { + if (self::$ffi) { + return; + } + // Try if preloaded + try { + self::$ffi = \FFI::scope("_REMI_ZSTD_"); + } catch (\FFI\Exception $e) { + // Try direct load + if (PHP_SAPI === 'cli' || (int)ini_get("ffi.enable")) { + self::$ffi = \FFI::load(__DIR__ . '/preload-zstd.h'); + } else { + throw $e; + } + } + if (!self::$ffi) { + throw new \RuntimeException("FFI parse fails"); + } + } + + public static function compress($in, $out) { + self::init(); + + $ret = []; + $src = file_get_contents($in); + if ($src === false) { + throw new \RuntimeException("Read fails"); + } + $len = strlen($src); + $ret['in_len'] = $len; + + $max = self::$ffi->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 @@ +<?php +foreach (glob(__DIR__ . '/preload-*.h') as $f) { + \FFI::load($f); +} +foreach (glob(__DIR__ . '/preload-*.inc') as $f) { + opcache_compile_file($f); +} + diff --git a/redis.php b/redis.php new file mode 100644 index 0000000..aacc581 --- /dev/null +++ b/redis.php @@ -0,0 +1,69 @@ +<?php +/** + * Redis connector using FFI and libhiredis + * PoC, only for documentation purpose + * + * Copyright (c) 2019 Remi Collet + * License: CC-BY-SA + * http://creativecommons.org/licenses/by-sa/4.0/ + */ +namespace Remi; + +if (PHP_VERSION_ID < 70400 || !extension_loaded("ffi")) { + die("PHP 7.4 with ffi extension required\n"); +} +printf("PHP version %s\n", PHP_VERSION); +if (PHP_SAPI == "cli" && !class_exists("\\Remi\\Redis")) { + printf("Fallback on manual load\n\n"); + require_once __DIR__ . '/preload-redis.inc'; +} else { + printf("Use preloaded class\n\n"); +} + +// ---------------- TESTS ------------------------ +$r = new Redis("localhost", 6379, true); +try { + var_dump($r->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 @@ +<?php +/** + * ZSTD compressor using FFI and libzstd + * PoC, only for documentation purpose + * + * Copyright (c) 2019 Remi Collet + * License: CC-BY-SA + * http://creativecommons.org/licenses/by-sa/4.0/ + */ + +if (PHP_VERSION_ID < 70400 || !extension_loaded("ffi")) { + die("PHP 7.4 with FFI required\n"); +} +printf("PHP version %s\n", PHP_VERSION); + +if (PHP_SAPI == "cli" && !class_exists("\\Remi\\Zstd")) { + printf("Fallback on manual load\n\n"); + require_once __DIR__ . '/preload-zstd.inc'; +} else { + printf("Use preloaded class\n\n"); +} +if (class_exists("\\Remi\\Zstd")) { + $t = microtime(true); + + $ret = \Remi\Zstd::compress(PHP_BINARY, "testffi.zstd"); + printf("Src length = %d\n", $ret['in_len']); + printf("ZSTD_compressBound = %d\n", $ret['max_len']); + printf("ZSTD_compress = %d\n", $ret['out_len']); + + $ret = \Remi\Zstd::decompress("testffi.zstd", "testffi.orig"); + printf("Src length = %d\n", $ret['in_len']); + printf("ZSTD_decompressBound = %d\n", $ret['max_len']); + printf("ZSTD_decompress = %d\n", $ret['out_len']); + + $t = microtime(true) - $t; + printf("Using FFI extension = %.3f\"\n\n", $t); +} else { + printf("FFI missing\n\n"); +} + +if (extension_loaded("zstd")) { + $t = microtime(true); + $src = file_get_contents(PHP_BINARY); + $len = strlen($src); + printf("Src length = %d\n", $len); + + $comp = zstd_compress($src, 6); + $clen = strlen($comp); + printf("ZSTD_compress = %d\n", $clen); + $comp = substr($comp, 0, $clen); + file_put_contents("testzstd.zstd", $comp); + + $comp = file_get_contents("testzstd.zstd"); + $unco = zstd_uncompress($comp); + $ulen = strlen($unco); + printf("ZSTD_decompress = %d\n", $ulen); + file_put_contents("testzstd.orig", $unco); + + var_dump($src === $unco); + $t = microtime(true) - $t; + printf("Using ZSTD extension = %.3f\"\n\n", $t); +} else { + printf("ZSTD missing\n\n"); +} + + |