diff options
Diffstat (limited to 'preload-redis.inc')
-rw-r--r-- | preload-redis.inc | 246 |
1 files changed, 246 insertions, 0 deletions
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"))); + } +} + |