summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README6
-rw-r--r--preload-redis.h83
-rw-r--r--preload-redis.inc246
-rw-r--r--preload-zstd.h9
-rw-r--r--preload-zstd.inc89
-rw-r--r--preload.php8
-rw-r--r--redis.php69
-rw-r--r--zstd.php66
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*
+
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 @@
+<?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");
+}
+
+