Adding libglim as an external library
libglim is an Apache-licensed C++ wrapper for lmdb, and rather than rolling our own it seems prudent to use it. Note: lmdb is not included in it, and unless something happens as did with libunbound, should be installed via each OS' package manager or equivalent.
This commit is contained in:
parent
07733f98c0
commit
90d6f8bf62
|
@ -0,0 +1,62 @@
|
||||||
|
Wed Dec 6 23:50:10 Russian Standard Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Fixes discovered under FreeBSD.
|
||||||
|
|
||||||
|
Wed Dec 6 04:43:04 Russian Standard Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Ported to Gentoo gcc 4.1.1
|
||||||
|
|
||||||
|
Fri Nov 24 14:17:08 Russian Standard Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Documentation improvements for the release.
|
||||||
|
|
||||||
|
Fri Nov 24 13:13:50 Russian Standard Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* A few fixes.
|
||||||
|
|
||||||
|
Fri Nov 24 13:13:20 Russian Standard Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Do not remove the "html" directory when cleaning.
|
||||||
|
|
||||||
|
Wed Nov 22 15:30:56 Russian Standard Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Added a compatibility #pair method to bufstr_t.
|
||||||
|
|
||||||
|
Tue Nov 21 21:54:40 Russian Standard Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Implemented the sugared way to querying.
|
||||||
|
|
||||||
|
Mon Oct 9 23:19:11 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Fixed incorrect statement = NULL in #step. Implemented parameter bindings.
|
||||||
|
|
||||||
|
Mon Oct 9 20:09:17 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* SqliteQuery and SqliteParQuery classes (no binding support as of yet).
|
||||||
|
|
||||||
|
Fri Oct 6 12:11:43 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* typo
|
||||||
|
|
||||||
|
Thu Oct 5 23:26:21 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Basic mutex operations in SqliteSession.
|
||||||
|
|
||||||
|
Sun Oct 1 23:19:36 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Compatibility with std::string.
|
||||||
|
|
||||||
|
Fri Sep 29 11:42:09 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Invented SqliteSession.
|
||||||
|
|
||||||
|
Fri Sep 29 01:23:31 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* SQLite wrapper: initial documentation; opening, closing.
|
||||||
|
|
||||||
|
Thu Sep 28 23:15:37 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Apache version 2 license.
|
||||||
|
|
||||||
|
Thu Sep 28 23:12:41 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Multiple source files for tests.
|
||||||
|
|
||||||
|
Thu Sep 28 01:05:21 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Append from another bufstr_t and from a pair.
|
||||||
|
|
||||||
|
Thu Sep 28 01:04:46 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Macro to construct pair from C character array.
|
||||||
|
|
||||||
|
Tue Sep 26 23:23:40 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* char const* instead of const char*
|
||||||
|
|
||||||
|
Tue Sep 26 20:56:07 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Everything seems to work. The library is now headers-only.
|
||||||
|
|
||||||
|
Mon Sep 25 22:52:34 Russian Daylight Time 2006 ArtemGr <artem@bizlink.ru>
|
||||||
|
* Initial revision, containing bufstr_t; compiles, but does not work.
|
|
@ -0,0 +1,13 @@
|
||||||
|
Copyright 2006-2012 Kozarezov Artem Aleksandrovich <artemciy@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,51 @@
|
||||||
|
#ifndef _NSEC_TIMER_H
|
||||||
|
#define _NSEC_TIMER_H
|
||||||
|
|
||||||
|
#include <time.h> // clock_gettime, CLOCK_MONOTONIC
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
//! Safe nanoseconds timer.
|
||||||
|
struct NsecTimer {
|
||||||
|
timespec start;
|
||||||
|
NsecTimer () {restart();}
|
||||||
|
//! Nanoseconds since the creation or restart of the timer.
|
||||||
|
int64_t operator()() const {
|
||||||
|
timespec nsecStop; clock_gettime (CLOCK_MONOTONIC, &nsecStop);
|
||||||
|
return (int64_t) (nsecStop.tv_sec - start.tv_sec) * 1000000000LL + (int64_t) (nsecStop.tv_nsec - start.tv_nsec);
|
||||||
|
}
|
||||||
|
/** Seconds since the creation or restart of the timer. */
|
||||||
|
double sec() const {
|
||||||
|
timespec nsecStop; clock_gettime (CLOCK_MONOTONIC, &nsecStop);
|
||||||
|
double seconds = nsecStop.tv_sec - start.tv_sec;
|
||||||
|
seconds += (double)(nsecStop.tv_nsec - start.tv_nsec) / 1000000000.0;
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
//! Seconds since the creation or restart of the timer.
|
||||||
|
std::string seconds (int precision = 9) const {
|
||||||
|
// The trick is to avoid the scientific notation by printing integers.
|
||||||
|
double sec = this->sec();
|
||||||
|
std::ostringstream buf;
|
||||||
|
int isec = (int) sec;
|
||||||
|
buf << isec;
|
||||||
|
|
||||||
|
sec -= isec;
|
||||||
|
for (int pc = precision; pc; --pc) sec *= 10.0;
|
||||||
|
int ifrac = (int) sec;
|
||||||
|
if (ifrac > 0) {
|
||||||
|
buf << '.';
|
||||||
|
buf.fill ('0'); buf.width (precision);
|
||||||
|
buf << ifrac;
|
||||||
|
}
|
||||||
|
return buf.str();
|
||||||
|
}
|
||||||
|
void restart() {clock_gettime (CLOCK_MONOTONIC, &start);}
|
||||||
|
int64_t getAndRestart() {int64_t tmp = operator()(); restart(); return tmp;}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _NSEC_TIMER_H
|
|
@ -0,0 +1,236 @@
|
||||||
|
#ifndef _GLIM_SERIALIZABLEPOOL_HPP_INCLUDED
|
||||||
|
#define _GLIM_SERIALIZABLEPOOL_HPP_INCLUDED
|
||||||
|
|
||||||
|
#include "gstring.hpp"
|
||||||
|
#ifndef _SERIALIZABLEPOOL_NOLDB
|
||||||
|
# include "ldb.hpp" // Reuse `ldbSerialize` and `ldbDeserialize` in the `with` method.
|
||||||
|
#endif
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
namespace SerializablePoolHelpers {
|
||||||
|
struct Impl {
|
||||||
|
/**
|
||||||
|
* Pool format: \code
|
||||||
|
* uint32_t valuesStart; // Network byte order.
|
||||||
|
* uint32_t value0Offset, value1Offset, ... valueNOffset; // Network byte order.
|
||||||
|
* char value0[]; char zero; char value1[]; char zero; ... char valueN[]; char zero;
|
||||||
|
* \endcode
|
||||||
|
*/
|
||||||
|
gstring _pool;
|
||||||
|
std::vector<gstring> _changes;
|
||||||
|
std::vector<bool> _changed;
|
||||||
|
bool _readOnly = false;
|
||||||
|
Impl() = default;
|
||||||
|
Impl (const gstring& poolBytes): _pool (poolBytes), _readOnly (false) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Can be used to avoid a deep copy of the pool and change vectors (pImpl idiom).
|
||||||
|
/// Example: \code glim::SerializablePool pool; \endcode
|
||||||
|
struct SharedPtr {
|
||||||
|
std::shared_ptr<Impl> _impl;
|
||||||
|
SharedPtr() = default;
|
||||||
|
SharedPtr (const gstring& poolBytes): _impl (std::make_shared<Impl> (poolBytes)) {}
|
||||||
|
Impl* get() const {return _impl.get();}
|
||||||
|
Impl* operator->() const {return _impl.get();}
|
||||||
|
Impl& operator*() const {return *_impl;}
|
||||||
|
Impl* instance() {if (!_impl) _impl = std::make_shared<Impl>(); return _impl.get();}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Can be used instead of SharedPtr to avoid the shared_ptr indirection when the SerializablePoolTpl isn't going to be copied.
|
||||||
|
/// Example: \code glim::InlineSerializablePool temporaryPool (bytes); \endcode
|
||||||
|
struct InlinePtr {
|
||||||
|
Impl _impl;
|
||||||
|
InlinePtr() = default;
|
||||||
|
InlinePtr (const gstring& poolBytes): _impl (poolBytes) {}
|
||||||
|
Impl* get() const {return const_cast<Impl*> (&_impl);}
|
||||||
|
Impl* operator->() const {return const_cast<Impl*> (&_impl);}
|
||||||
|
Impl& operator*() const {return const_cast<Impl&> (_impl);}
|
||||||
|
Impl* instance() {return &_impl;}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Serialization with lazy parsing: fields are accessed without "unpacking" the byte array.
|
||||||
|
* Changes are stored separately, allowing the user to know exactly what fields has been changed and compare the old values to the new ones. */
|
||||||
|
template <typename PI>
|
||||||
|
class SerializablePoolTpl {
|
||||||
|
protected:
|
||||||
|
using Impl = SerializablePoolHelpers::Impl;
|
||||||
|
PI _impl;
|
||||||
|
/** @param ref Return a zero-copy view. The view should not be used outside of the pool buffer's lifetime. */
|
||||||
|
static gstring original (const gstring& pool, uint32_t num, bool ref = false) {
|
||||||
|
uint32_t poolLength = pool.length(); if (poolLength < 4) return gstring();
|
||||||
|
uint32_t valuesStart = ntohl (*(uint32_t*) pool.data());
|
||||||
|
assert (valuesStart <= poolLength);
|
||||||
|
uint32_t valueOffsetOffset = 4 + num * 4;
|
||||||
|
if ((int) valuesStart - (int) valueOffsetOffset < 4) return gstring(); // num > size
|
||||||
|
uint32_t valueOffset = ntohl (*(uint32_t*) (pool.data() + valueOffsetOffset));
|
||||||
|
valueOffsetOffset += 4;
|
||||||
|
uint32_t nextValueOffset = ((int) valuesStart - (int) valueOffsetOffset < 4)
|
||||||
|
? poolLength
|
||||||
|
: ntohl (*(uint32_t*) (pool.data() + valueOffsetOffset));
|
||||||
|
return gstring (0, (void*) (pool.data() + valueOffset), false, nextValueOffset - 1 - valueOffset, ref);
|
||||||
|
}
|
||||||
|
/** How many elements are in the pool. */
|
||||||
|
static uint32_t poolSize (const gstring& pool) {
|
||||||
|
if (pool.length() < 4) return 0;
|
||||||
|
uint32_t valuesStart = ntohl (*(uint32_t*) pool.data());
|
||||||
|
return (valuesStart - 4) / 4;
|
||||||
|
}
|
||||||
|
void toBytes (gstring& newPool, uint32_t size, const gstring* oldPool) const {
|
||||||
|
newPool.clear();
|
||||||
|
const Impl* impl = _impl.get();
|
||||||
|
const std::vector<bool>& changed = impl->_changed;
|
||||||
|
const std::vector<gstring>& changes = impl->_changes;
|
||||||
|
if (changed.empty()) return;
|
||||||
|
uint32_t valuesStart = 4 + size * 4;
|
||||||
|
uint32_t networkOrder = 0;
|
||||||
|
newPool.append ((char*) &(networkOrder = htonl (valuesStart)), 4);
|
||||||
|
for (uint32_t num = 0; num < size; ++num) newPool.append ((char*) &(networkOrder = 0), 4);
|
||||||
|
for (uint32_t num = 0; num < size; ++num) {
|
||||||
|
uint32_t start = newPool.length();
|
||||||
|
if (num < changed.size() && changed[num]) newPool << changes[num];
|
||||||
|
else newPool << original (oldPool ? *oldPool : impl->_pool, num);
|
||||||
|
newPool << '\0';
|
||||||
|
uint32_t valuesOffsetOffset = 4 + num * 4; assert (valuesOffsetOffset < valuesStart);
|
||||||
|
*(uint32_t*)(newPool.data() + valuesOffsetOffset) = htonl (start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
/** Field, old value, new value. Might be used to maintain indexes. */
|
||||||
|
typedef std::function<void(uint32_t, const gstring&, const gstring&)> ChangeVisitor;
|
||||||
|
|
||||||
|
SerializablePoolTpl() = default;
|
||||||
|
/** Copy the given pool bytes from the outside source (e.g. from the database). */
|
||||||
|
SerializablePoolTpl (const gstring& poolBytes): _impl (poolBytes) {}
|
||||||
|
/** Returns a view into the original serialized field (ignores the current changes).\n
|
||||||
|
* Returns an empty string if the field is not in the pool (num > size).
|
||||||
|
* @param ref Return a zero-copy view. The view becomes invalid after the value has been changed or when the pool's `Impl` is destroyed. */
|
||||||
|
const gstring original (uint32_t num, bool ref = false) const {return original (_impl->_pool, num, ref);}
|
||||||
|
/** Returns the original serialized field (ignores the current changes).\n
|
||||||
|
* Returns an empty string if the field is not in the pool (num > size). */
|
||||||
|
const char* cstringOriginal (uint32_t num) const {
|
||||||
|
gstring gs (original (_impl->_pool, num));
|
||||||
|
return gs.empty() ? "" : gs.data(); // All fields in the _pool are 0-terminated.
|
||||||
|
}
|
||||||
|
/** Returns the field.
|
||||||
|
* @param ref Return a zero-copy view. The view becomes invalid after the value has been changed or when the pool's `Impl` is destroyed. */
|
||||||
|
const gstring current (uint32_t num, bool ref = false) const {
|
||||||
|
const Impl* impl = _impl.get(); if (!impl) return gstring();
|
||||||
|
if (num < impl->_changed.size() && impl->_changed[num]) {
|
||||||
|
const gstring& value = impl->_changes[num]; return ref ? value.ref() : value;}
|
||||||
|
return original (impl->_pool, num);
|
||||||
|
}
|
||||||
|
/** Set the new value of the field. */
|
||||||
|
void set (uint32_t num, const gstring& value) {
|
||||||
|
Impl* impl = _impl.instance();
|
||||||
|
if (__builtin_expect (impl->_readOnly, 0)) throw std::runtime_error ("Attempt to modify a read-only SerializablePool");
|
||||||
|
if (num >= impl->_changed.size()) {impl->_changed.resize (num + 1); impl->_changes.resize (num + 1);}
|
||||||
|
impl->_changed[num] = true;
|
||||||
|
impl->_changes[num] = value;
|
||||||
|
}
|
||||||
|
void reserve (uint32_t fields) {
|
||||||
|
Impl* impl = _impl.instance();
|
||||||
|
if (__builtin_expect (impl->_readOnly, 0)) throw std::runtime_error ("Attempt to modify a read-only SerializablePool");
|
||||||
|
impl->_changed.reserve (fields);
|
||||||
|
impl->_changes.reserve (fields);
|
||||||
|
}
|
||||||
|
/** Peek into the pool.\n
|
||||||
|
* Returned reference should not be used after the SerializablePool goes out of scope (and destroyed). */
|
||||||
|
const gstring& originalPool() {return _impl->_pool;}
|
||||||
|
/** Serialize the pool.
|
||||||
|
* @param changeVisitor is called for every field that was really changed (e.g. the bytes differ). */
|
||||||
|
void toBytes (gstring& newPool, ChangeVisitor changeVisitor = ChangeVisitor()) const {
|
||||||
|
if (changeVisitor) {
|
||||||
|
const Impl* impl = _impl.get();
|
||||||
|
const std::vector<bool>& changed = impl->_changed;
|
||||||
|
const std::vector<gstring>& changes = impl->_changes;
|
||||||
|
for (uint32_t num = 0, size = changed.size(); num < size; ++num) if (changed[num]) {
|
||||||
|
const gstring& from = original (impl->_pool, num); const gstring& to = changes[num];
|
||||||
|
if (from != to) changeVisitor (num, from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toBytes (newPool, (uint32_t) _impl->_changed.size(), nullptr);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Performs "delta" serialization of the pool: creates a new pool where values which has not changed are copied from the `oldPool`.\n
|
||||||
|
* \code Use case: 1) pools X and Y are loaded from a database by users A and B;
|
||||||
|
* 2) user A changes field 0 in pool X; 3) user B changes field 1 in pool Y;
|
||||||
|
* 4) user A loads `oldPool` from the database, does `toBytesDelta` from pool X and saves to the database;
|
||||||
|
* 5) user B loads `oldPool` from the database, does `toBytesDelta` from pool Y and saves to the database;
|
||||||
|
* result: database contains both changes (field 0 from user A and field 1 from user B). \endcode
|
||||||
|
* @param changeVisitor is called for every field that was changed between the oldPool and the current one.
|
||||||
|
* Returns `false` and leaves `newPool` *empty* if there are no changes found against the `oldPool`.
|
||||||
|
*/
|
||||||
|
bool toBytesDelta (gstring& newPool, const gstring& oldPool, ChangeVisitor changeVisitor = ChangeVisitor()) const {
|
||||||
|
newPool.clear();
|
||||||
|
const Impl* impl = _impl.get();
|
||||||
|
const std::vector<bool>& changed = impl->_changed;
|
||||||
|
const std::vector<gstring>& changes = impl->_changes;
|
||||||
|
bool verifiedChanges = false;
|
||||||
|
for (uint32_t num = 0, size = changed.size(); num < size; ++num) if (changed[num]) {
|
||||||
|
const gstring& from = original (oldPool, num); const gstring& to = changes[num];
|
||||||
|
if (from != to) {
|
||||||
|
verifiedChanges = true;
|
||||||
|
if (changeVisitor) changeVisitor (num, from, to); else break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!verifiedChanges) return false;
|
||||||
|
|
||||||
|
toBytes (newPool, std::max ((uint32_t) changed.size(), poolSize (oldPool)), &oldPool);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/** True if the field has been `set` in this pool instance.\n
|
||||||
|
* NB: Does *not* check if the `set` value is equal to the `original` value or not. */
|
||||||
|
bool changed (uint32_t num) const {const auto& changed = _impl->_changed; return num < changed.size() ? changed[num] : false;}
|
||||||
|
/** True if a field has been `set` in this pool instance. */
|
||||||
|
bool changed() const {return !_impl->_changed.empty();}
|
||||||
|
|
||||||
|
bool operator == (const SerializablePoolTpl<PI>& rhs) const {return _impl.get() == rhs._impl.get();}
|
||||||
|
/** Useful for storing SerializablePool in a map. */
|
||||||
|
intptr_t implId() const {return (intptr_t) _impl.get();}
|
||||||
|
|
||||||
|
/** If set to `true` then modifying the pool will throw an exception.\n
|
||||||
|
* Useful for freezing the pool before sharing it with other threads. */
|
||||||
|
void readOnly (bool ro) {if (_impl) _impl->_readOnly = true;}
|
||||||
|
bool readOnly() const {return (_impl ? _impl->_readOnly : false);}
|
||||||
|
|
||||||
|
/** Number of elements in the pool. Equals to max(num)-1. */
|
||||||
|
uint32_t size() const {
|
||||||
|
Impl* impl = _impl.get(); if (__builtin_expect (!impl, 0)) return 0;
|
||||||
|
return std::max (poolSize (impl->_pool), (uint32_t) impl->_changed.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _SERIALIZABLEPOOL_NOLDB
|
||||||
|
/** Serialize the `value` with `ldbSerialize` and `set` it to `num`.
|
||||||
|
* @param stackSize is the amount of space to preallocate on stack for the temporary buffer. */
|
||||||
|
template<typename T> void serialize (uint32_t num, const T& value, uint32_t stackSize = 256) {
|
||||||
|
GSTRING_ON_STACK (bytes, stackSize);
|
||||||
|
ldbSerialize (bytes, value);
|
||||||
|
set (num, bytes);
|
||||||
|
}
|
||||||
|
/** If the field is not empty then `ldbDeserialize` it into `value`. */
|
||||||
|
template<typename T> void deserialize (uint32_t num, T& value) const {
|
||||||
|
const gstring& bytes = current (num);
|
||||||
|
if (bytes.length()) ldbDeserialize (current (num), value);
|
||||||
|
}
|
||||||
|
/** Deserialize the `num` field with `ldbDeserialize`, run `visitor` on it, then optionally serialize the field back using `ldbSerialize`.
|
||||||
|
* Example: \code
|
||||||
|
* typedef std::map<std::string, std::string> MyMap;
|
||||||
|
* pool.with<MyMap> (_myMap, [](MyMap& myMap) {myMap["foo"] = "bar"; return true;});
|
||||||
|
* \endcode
|
||||||
|
* @param visitor must return `true` to serialize the field back to the pool.
|
||||||
|
*/
|
||||||
|
template<typename T> void with (uint32_t num, std::function<bool(T&)> visitor) {
|
||||||
|
const gstring& fromBytes = current (num, true);
|
||||||
|
T value; if (fromBytes.length()) ldbDeserialize (fromBytes, value);
|
||||||
|
if (visitor (value)) serialize (num, value, 16 + fromBytes.length() * 2);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
using SerializablePool = SerializablePoolTpl<SerializablePoolHelpers::SharedPtr>;
|
||||||
|
using InlineSerializablePool = SerializablePoolTpl<SerializablePoolHelpers::InlinePtr>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _GLIM_SERIALIZABLEPOOL_HPP_INCLUDED
|
|
@ -0,0 +1,32 @@
|
||||||
|
#ifndef _TSC_TIMER_H
|
||||||
|
#define _TSC_TIMER_H
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
extern "C" { // http://en.wikipedia.org/wiki/Rdtsc
|
||||||
|
#if (defined(__GNUC__) || defined(__ICC)) && defined(__i386__)
|
||||||
|
static __inline__ unsigned long long rdTsc(void) {
|
||||||
|
unsigned long long ret;
|
||||||
|
__asm__ __volatile__("rdtsc": "=A" (ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
#elif (defined(__GNUC__) || defined(__ICC) || defined(__SUNPRO_C)) && defined(__x86_64__)
|
||||||
|
static __inline__ unsigned long long rdTsc(void) {
|
||||||
|
unsigned a, d;
|
||||||
|
asm volatile("rdtsc" : "=a" (a), "=d" (d));
|
||||||
|
return ((unsigned long long)a) | (((unsigned long long)d) << 32);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
//! CPU cycles timer. Fast, not safe.
|
||||||
|
//! cf. http://en.wikipedia.org/wiki/Rdtsc
|
||||||
|
struct TscTimer {
|
||||||
|
int64_t start;
|
||||||
|
TscTimer (): start (rdTsc()) {}
|
||||||
|
int64_t operator()() const {return rdTsc() - start;}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _TSC_TIMER_H
|
|
@ -0,0 +1,203 @@
|
||||||
|
/** \file
|
||||||
|
* ucontext-based coroutine library designed to emulate a normal control flow around callbacks. */
|
||||||
|
|
||||||
|
// http://en.wikipedia.org/wiki/Setcontext; man 3 makecontext; man 2 getcontext
|
||||||
|
// http://www.boost.org/doc/libs/1_53_0/libs/context/doc/html/index.html
|
||||||
|
// g++ -std=c++11 -O1 -Wall -g test_cbcoro.cc -pthread && ./a.out
|
||||||
|
|
||||||
|
// NB: There is now a coroutine support in Boost ASIO which can be used to make asynchronous APIs look synchronous in a similar way:
|
||||||
|
// https://svn.boost.org/trac/boost/changeset/84311
|
||||||
|
|
||||||
|
#include <ucontext.h>
|
||||||
|
#include <sys/mman.h> // mmap
|
||||||
|
#include <string.h> // strerror
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <valgrind/valgrind.h>
|
||||||
|
#include <glim/exception.hpp>
|
||||||
|
#include <boost/container/flat_map.hpp>
|
||||||
|
#include <boost/container/slist.hpp>
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
/// Simplifies turning callback control flows into normal imperative control flows.
|
||||||
|
class CBCoro {
|
||||||
|
public:
|
||||||
|
/// "Holds" the CBCoro and will delete it when it is no longer used.
|
||||||
|
struct CBCoroPtr {
|
||||||
|
CBCoro* _coro;
|
||||||
|
CBCoroPtr (CBCoro* coro): _coro (coro) {
|
||||||
|
_coro->_users++;
|
||||||
|
}
|
||||||
|
~CBCoroPtr() {
|
||||||
|
if (--_coro->_users <= 0 && _coro->_delete) delete _coro;
|
||||||
|
}
|
||||||
|
CBCoro* operator ->() const {return _coro;}
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr size_t defaultStackSize() {return 512 * 1024;}
|
||||||
|
static constexpr uint8_t defaultCacheSize() {return 2;}
|
||||||
|
protected:
|
||||||
|
typedef boost::container::flat_map<size_t, boost::container::slist<void*> > cache_t;
|
||||||
|
/// The cached stacks; stackSize -> free list.
|
||||||
|
static cache_t& cache() {static cache_t CACHE; return CACHE;}
|
||||||
|
static std::mutex& cacheMutex() {static std::mutex CACHE_MUTEX; return CACHE_MUTEX;}
|
||||||
|
|
||||||
|
ucontext_t _context;
|
||||||
|
ucontext_t* _returnTo;
|
||||||
|
std::recursive_mutex _mutex; ///< This one is locked most of the time.
|
||||||
|
std::atomic_int_fast32_t _users; ///< Counter used by `CBCoroPtr`.
|
||||||
|
bool _delete; ///< Whether the `CBCoroPtr` should `delete` this instance when it is no longer used (default is `true`).
|
||||||
|
bool _invokeFromYield; ///< True if `invokeFromCallback()` was called directly from `yieldForCallback()`.
|
||||||
|
bool _yieldFromInvoke; ///< True if `yieldForCallback()` now runs from `invokeFromCallback()`.
|
||||||
|
uint8_t const _cacheStack; ///< Tells `freeStack()` to cache the stack if the number of cached `#_stackSize` stacks is less than it.
|
||||||
|
void* _stack;
|
||||||
|
size_t const _stackSize; ///< Keeps the size of the stack.
|
||||||
|
|
||||||
|
/// Peek a stack from the cache or allocate one with `mmap` (and register with Valgrind).
|
||||||
|
virtual void allocateStack() {
|
||||||
|
if (_cacheStack) {
|
||||||
|
std::lock_guard<std::mutex> lock (cacheMutex());
|
||||||
|
auto& freeList = cache()[_stackSize];
|
||||||
|
if (!freeList.empty()) {_stack = freeList.front(); freeList.pop_front(); return;}
|
||||||
|
}
|
||||||
|
_stack = mmap (nullptr, _stackSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK | MAP_NORESERVE, -1, 0);
|
||||||
|
if (_stack == MAP_FAILED) GTHROW (std::string ("mmap allocation failed: ") + ::strerror (errno));
|
||||||
|
#pragma GCC diagnostic ignored "-Wunused-value"
|
||||||
|
VALGRIND_STACK_REGISTER (_stack, (char*) _stack + _stackSize);
|
||||||
|
}
|
||||||
|
/// Release a stack into the cache or free it with `munmap` (and deregister with Valgrind).
|
||||||
|
virtual void freeStack() {
|
||||||
|
if (_cacheStack) {
|
||||||
|
std::lock_guard<std::mutex> lock (cacheMutex());
|
||||||
|
auto& freeList = cache()[_stackSize];
|
||||||
|
if (freeList.size() < _cacheStack) {freeList.push_front (_stack); _stack = nullptr; return;}
|
||||||
|
}
|
||||||
|
VALGRIND_STACK_DEREGISTER (_stack);
|
||||||
|
if (munmap (_stack, _stackSize)) GTHROW (std::string ("!munmap: ") + ::strerror (errno));;
|
||||||
|
_stack = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the coroutine (initialize context, allocate stack and register it with Valgrind).
|
||||||
|
CBCoro (uint8_t cacheStack = defaultCacheSize(), size_t stackSize = defaultStackSize()):
|
||||||
|
_returnTo (nullptr), _users (0), _delete (true), _invokeFromYield (false), _yieldFromInvoke (false),
|
||||||
|
_cacheStack (cacheStack), _stack (nullptr), _stackSize (stackSize) {
|
||||||
|
if (getcontext (&_context)) GTHROW ("!getcontext");
|
||||||
|
allocateStack();
|
||||||
|
_context.uc_stack.ss_sp = _stack;
|
||||||
|
_context.uc_stack.ss_size = stackSize;
|
||||||
|
}
|
||||||
|
virtual ~CBCoro() {
|
||||||
|
freeStack();
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
/// Starts the coroutine on the `_stack` (makecontext, swapcontext), calling the `CBCoro::run`.
|
||||||
|
CBCoroPtr start() {
|
||||||
|
CBCoroPtr ptr (this);
|
||||||
|
ucontext_t back; _context.uc_link = &back;
|
||||||
|
makecontext (&_context, (void(*)()) cbcRun, 1, (intptr_t) this);
|
||||||
|
// Since we have to "return" from inside the `yieldForCallback`,
|
||||||
|
// we're not actually using the `_context.uc_link` and `return`, we use `setcontext (_returnTo)` instead.
|
||||||
|
_returnTo = &back;
|
||||||
|
_mutex.lock();
|
||||||
|
swapcontext (&back, &_context); // Now our stack lives and the caller stack is no longer in control.
|
||||||
|
_mutex.unlock();
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
protected:
|
||||||
|
/// Logs exception thrown from `CBCoro::run`.
|
||||||
|
virtual void log (const std::exception& ex) {
|
||||||
|
std::cerr << "glim::CBCoro, exception: " << ex.what() << std::endl;
|
||||||
|
}
|
||||||
|
static void cbcRun (CBCoro* cbCoro) {
|
||||||
|
try {
|
||||||
|
cbCoro->run();
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
cbCoro->log (ex);
|
||||||
|
}
|
||||||
|
cbCoro->cbcReturn(); // Return the control to the rightful owner, e.g. to a last callback who ran `invokeFromCallback`, or otherwise to `cbcStart`.
|
||||||
|
}
|
||||||
|
/// Relinquish the control to the original owner of the thread, restoring its stack.
|
||||||
|
void cbcReturn() {
|
||||||
|
ucontext_t* returnTo = _returnTo;
|
||||||
|
if (returnTo != nullptr) {_returnTo = nullptr; setcontext (returnTo);}
|
||||||
|
}
|
||||||
|
/// This method is performed on the CBCoro stack, allowing it to be suspended and then reanimated from callbacks.
|
||||||
|
virtual void run() = 0;
|
||||||
|
public:
|
||||||
|
/** Use this method to wrap a return-via-callback code.
|
||||||
|
* For example, the callback code \code
|
||||||
|
* startSomeWork ([=]() {
|
||||||
|
* continueWhenWorkIsFinished();
|
||||||
|
* });
|
||||||
|
* \endcode should be turned into \code
|
||||||
|
* yieldForCallback ([&]() {
|
||||||
|
* startSomeWork ([&]() {
|
||||||
|
* invokeFromCallback();
|
||||||
|
* });
|
||||||
|
* });
|
||||||
|
* continueWhenWorkIsFinished();
|
||||||
|
* \endcode
|
||||||
|
*
|
||||||
|
* Captures the stack, runs the `fun` and relinquish the control to `_returnTo`.\n
|
||||||
|
* This method will never "return" by itself, in order for it to "return" the
|
||||||
|
* `fun` MUST call `invokeFromCallback`, maybe later and from a different stack. */
|
||||||
|
template <typename F> CBCoroPtr yieldForCallback (F fun) {
|
||||||
|
CBCoroPtr ptr (this);
|
||||||
|
_yieldFromInvoke = false;
|
||||||
|
if (getcontext (&_context)) GTHROW ("!getcontext"); // Capture.
|
||||||
|
if (_yieldFromInvoke) {
|
||||||
|
// We're now in the future, revived by the `invokeFromCallback`.
|
||||||
|
// All we do now is "return" to the caller whose stack we captured earlier.
|
||||||
|
} else {
|
||||||
|
// We're still in the present, still have some work to do.
|
||||||
|
fun(); // The `fun` is supposed to do something resulting in the `invokeFromCallback` being called later.
|
||||||
|
if (_invokeFromYield) {
|
||||||
|
// The `fun` used the `invokeFromCallback` directly, not resorting to callbacks, meaning we don't have to do our magick.
|
||||||
|
_invokeFromYield = false;
|
||||||
|
} else {
|
||||||
|
// So, the `fun` took measures to revive us later, it's time for us to go into torpor and return the control to whoever we've borrowed it from.
|
||||||
|
cbcReturn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To be called from a callback in order to lend the control to CBCoro, continuing it from where it called `yieldForCallback`.
|
||||||
|
CBCoroPtr invokeFromCallback() {
|
||||||
|
CBCoroPtr ptr (this);
|
||||||
|
_mutex.lock(); // Wait for an other-thready `yieldForCallback` to finish.
|
||||||
|
if (_returnTo != nullptr) {
|
||||||
|
// We have not yet "returned" from the `yieldForCallback`,
|
||||||
|
// meaning that the `invokeFromCallback` was executed immediately from inside the `yieldForCallback`.
|
||||||
|
// In that case we must DO NOTHING, we must simply continue running on the current stack.
|
||||||
|
_invokeFromYield = true; // Tells `yieldForCallback` to do nothing.
|
||||||
|
} else {
|
||||||
|
// Revive the CBCoro, letting it continue from where it was suspended in `yieldForCallback`.
|
||||||
|
ucontext_t cbContext; _returnTo = &cbContext; _yieldFromInvoke = true;
|
||||||
|
if (swapcontext (&cbContext, &_context)) GTHROW ("!swapcontext");
|
||||||
|
// NB: When the CBCoro is suspended or exits, the control returns back there and then back to the callback from which we borrowed it.
|
||||||
|
if (_returnTo == &cbContext) _returnTo = nullptr;
|
||||||
|
}
|
||||||
|
_mutex.unlock(); // Other-thready `yieldForCallback` has finished and `cbcReturn`ed here.
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** CBCoro running a given functor.
|
||||||
|
* The functor's first argument must be a CBCoro pointer, like this: \code (new CBCoroForFunctor ([](CBCoro* cbcoro) {}))->start(); \endcode */
|
||||||
|
template <typename FUN> struct CBCoroForFunctor: public CBCoro {
|
||||||
|
FUN _fun;
|
||||||
|
template <typename CFUN> CBCoroForFunctor (CFUN&& fun, uint8_t cacheStack, size_t stackSize): CBCoro (cacheStack, stackSize), _fun (std::forward<CFUN> (fun)) {}
|
||||||
|
virtual void run() {_fun (this);}
|
||||||
|
virtual ~CBCoroForFunctor() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Syntactic sugar: Runs a given functor in a CBCoro instance.
|
||||||
|
* Example: \code glim::cbCoro ([](glim::CBCoro* cbcoro) {}); \endcode
|
||||||
|
* Returns a `CBCoroPtr` to the CBCoro instance holding the `fun` which might be held somewhere in order to delay the deletion of `fun`. */
|
||||||
|
template <typename FUN> inline CBCoro::CBCoroPtr cbCoro (FUN&& fun, uint8_t cacheStack = CBCoro::defaultCacheSize(), size_t stackSize = CBCoro::defaultStackSize()) {
|
||||||
|
return (new CBCoroForFunctor<FUN> (std::forward<FUN> (fun), cacheStack, stackSize))->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
#ifndef _GLIM_CHANNEL_INCLUDED
|
||||||
|
#define _GLIM_CHANNEL_INCLUDED
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
/// Unbuffered channel.
|
||||||
|
/// Optimized for a single value (busy-waits on a second one).
|
||||||
|
template <typename V>
|
||||||
|
struct Channel {
|
||||||
|
V _v;
|
||||||
|
std::mutex _mutex; // Locked when there is no value.
|
||||||
|
std::atomic_int_fast8_t _state; enum State {EMPTY = 0, WRITING = 1, FULL = 2};
|
||||||
|
Channel(): _state (EMPTY) {_mutex.lock();}
|
||||||
|
// Waits until the Channel is empty then stores the value.
|
||||||
|
template <typename VA> void send (VA&& v) {
|
||||||
|
for (;;) {
|
||||||
|
int_fast8_t expectEmpty = EMPTY; if (_state.compare_exchange_weak (expectEmpty, WRITING)) break;
|
||||||
|
std::this_thread::sleep_for (std::chrono::milliseconds (20));
|
||||||
|
}
|
||||||
|
try {_v = std::forward<V> (v);} catch (...) {_state = EMPTY; throw;}
|
||||||
|
_state = FULL;
|
||||||
|
_mutex.unlock(); // Allows the reader to proceed.
|
||||||
|
}
|
||||||
|
// Waits untill there is a value to receive.
|
||||||
|
V receive() {
|
||||||
|
_mutex.lock(); // Wait.
|
||||||
|
V tmp = std::move (_v);
|
||||||
|
assert (_state == FULL);
|
||||||
|
_state = EMPTY;
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace glim
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,304 @@
|
||||||
|
/** \file
|
||||||
|
* Very simple header-only wrapper around libcurl.\n
|
||||||
|
* See also: https://github.com/venam/Browser\n
|
||||||
|
* See also: https://github.com/mologie/curl-asio\n
|
||||||
|
* See also: http://thread.gmane.org/gmane.comp.web.curl.library/1322 (this one uses a temporary file). */
|
||||||
|
|
||||||
|
#ifndef _GLIM_CURL_INCLUDED
|
||||||
|
#define _GLIM_CURL_INCLUDED
|
||||||
|
|
||||||
|
#include "gstring.hpp"
|
||||||
|
#include "exception.hpp"
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
inline size_t curlWriteToString (void *buffer, size_t size, size_t nmemb, void *userp) {
|
||||||
|
((std::string*) userp)->append ((const char*) buffer, size * nmemb);
|
||||||
|
return size * nmemb;};
|
||||||
|
|
||||||
|
inline size_t curlReadFromString (void *ptr, size_t size, size_t nmemb, void *userdata);
|
||||||
|
inline size_t curlReadFromGString (void *ptr, size_t size, size_t nmemb, void *userdata);
|
||||||
|
inline size_t curlWriteHeader (void *ptr, size_t size, size_t nmemb, void *curlPtr);
|
||||||
|
inline int curlDebugCB (CURL* curl, curl_infotype type, char* bytes, size_t size, void* curlPtr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
Simple HTTP requests using cURL.
|
||||||
|
Example: \code
|
||||||
|
std::string w3 = glim::Curl() .http ("http://www.w3.org/") .go().str();
|
||||||
|
\endcode
|
||||||
|
*/
|
||||||
|
class Curl {
|
||||||
|
protected:
|
||||||
|
Curl (const Curl&): _curl (NULL), _headers (NULL), _sent (0), _needs_cleanup (true) {} // No copying.
|
||||||
|
public:
|
||||||
|
struct PerformError: public glim::Exception {
|
||||||
|
PerformError (const char* message, const char* file, int32_t line):
|
||||||
|
glim::Exception (message, file, line) {}
|
||||||
|
};
|
||||||
|
struct GetinfoError: public glim::Exception {
|
||||||
|
CURLcode _code; std::string _error;
|
||||||
|
GetinfoError (CURLcode code, const std::string& error, const char* file, int32_t line):
|
||||||
|
glim::Exception (error, file, line),
|
||||||
|
_code (code), _error (error) {}
|
||||||
|
};
|
||||||
|
public:
|
||||||
|
CURL* _curl;
|
||||||
|
struct curl_slist *_headers;
|
||||||
|
std::function<void (const char* header, int len)> _headerListener;
|
||||||
|
std::function<void (curl_infotype type, char* bytes, size_t size)> _debugListener;
|
||||||
|
std::string _sendStr; ///< We're using `std::string` instead of `gstring` in order to support payloads larger than 16 MiB.
|
||||||
|
glim::gstring _sendGStr; ///< `gstring::view` and `gstring::ref` allow us to zero-copy.
|
||||||
|
uint32_t _sent;
|
||||||
|
std::string _got;
|
||||||
|
bool _needs_cleanup:1; ///< ~Curl will do `curl_easy_cleanup` if `true`.
|
||||||
|
char _errorBuf[CURL_ERROR_SIZE];
|
||||||
|
|
||||||
|
Curl (Curl&&) = default;
|
||||||
|
|
||||||
|
/// @param cleanup can be turned off if the cURL is freed elsewhere.
|
||||||
|
Curl (bool cleanup = true): _curl (curl_easy_init()), _headers (NULL), _sent (0), _needs_cleanup (cleanup) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_NOSIGNAL, 1L); // required per http://curl.haxx.se/libcurl/c/libcurl-tutorial.html#Multi-threading
|
||||||
|
*_errorBuf = 0;}
|
||||||
|
/// Wraps an existing handle (will invoke `curl_easy_cleanup` nevertheless).
|
||||||
|
/// @param cleanup can be turned off if the cURL is freed elsewhere.
|
||||||
|
Curl (CURL* curl, bool cleanup = true): _curl (curl), _headers (NULL), _sent (0), _needs_cleanup (cleanup) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_NOSIGNAL, 1L); // required per http://curl.haxx.se/libcurl/c/libcurl-tutorial.html#Multi-threading
|
||||||
|
*_errorBuf = 0;}
|
||||||
|
~Curl(){
|
||||||
|
if (_headers) {curl_slist_free_all (_headers); _headers = NULL;}
|
||||||
|
if (_curl) {if (_needs_cleanup) curl_easy_cleanup (_curl); _curl = NULL;}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stores the content to be sent into an `std::string` inside `Curl`.
|
||||||
|
* NB: In order to have an effect this method should be used *before* the `http()` and `smtp()` methods. */
|
||||||
|
template<typename STR> Curl& send (STR&& text) {
|
||||||
|
_sendStr = std::forward<STR> (text);
|
||||||
|
_sendGStr.clear();
|
||||||
|
_sent = 0;
|
||||||
|
return *this;}
|
||||||
|
|
||||||
|
/// Adds "Content-Type" header into `_headers`.
|
||||||
|
Curl& contentType (const char* ct) {
|
||||||
|
char ctb[64]; gstring cth (sizeof (ctb), ctb, false, 0);
|
||||||
|
cth << "Content-Type: " << ct << "\r\n";
|
||||||
|
_headers = curl_slist_append (_headers, cth.c_str());
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @param fullHeader is a full HTTP header and a newline, e.g. "User-Agent: Me\r\n".
|
||||||
|
Curl& header (const char* fullHeader) {
|
||||||
|
_headers = curl_slist_append (_headers, fullHeader);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Sets the majority of options for the http request.
|
||||||
|
NB: If `send` was used with a non-empty string then `http` will use `CURLOPT_UPLOAD`, setting http method to `PUT` (use the `method()` to override).
|
||||||
|
\n
|
||||||
|
Example: \code
|
||||||
|
glim::Curl curl;
|
||||||
|
curl.http (url.c_str()) .go();
|
||||||
|
std::cout << curl.status() << std::endl << curl.str() << std::endl;
|
||||||
|
\endcode
|
||||||
|
*/
|
||||||
|
Curl& http (const char* url, int timeoutSec = 20) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_NOSIGNAL, 1L); // required per http://curl.haxx.se/libcurl/c/libcurl-tutorial.html#Multi-threading
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_URL, url);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_WRITEFUNCTION, curlWriteToString);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_WRITEDATA, &_got);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_TIMEOUT, timeoutSec);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_ERRORBUFFER, _errorBuf);
|
||||||
|
if (_sendStr.size() || _sendGStr.size()) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_UPLOAD, 1L); // http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTUPLOAD
|
||||||
|
if (_sendStr.size()) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_INFILESIZE, (long) _sendStr.size());
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_READFUNCTION, curlReadFromString);
|
||||||
|
} else {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_INFILESIZE, (long) _sendGStr.size());
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_READFUNCTION, curlReadFromGString);}
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_READDATA, this);}
|
||||||
|
if (_headers)
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_HTTPHEADER, _headers); // http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTHTTPHEADER
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Set options for smtp request.
|
||||||
|
Example: \code
|
||||||
|
long rc = glim::Curl().send ("Subject: subject\r\n\r\n" "text\r\n") .smtp ("from", "to") .go().status();
|
||||||
|
if (rc != 250) std::cerr << "Error sending email: " << rc << std::endl;
|
||||||
|
\endcode */
|
||||||
|
Curl& smtp (const char* from, const char* to) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_NOSIGNAL, 1L); // required per http://curl.haxx.se/libcurl/c/libcurl-tutorial.html#Multi-threading
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_URL, "smtp://127.0.0.1");
|
||||||
|
if (from) curl_easy_setopt (_curl, CURLOPT_MAIL_FROM, from);
|
||||||
|
bcc (to);
|
||||||
|
if (_headers) curl_easy_setopt (_curl, CURLOPT_MAIL_RCPT, _headers);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_WRITEFUNCTION, curlWriteToString);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_WRITEDATA, &_got);
|
||||||
|
if (_sendStr.size()) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_INFILESIZE, (long) _sendStr.size());
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_READFUNCTION, curlReadFromString);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_READDATA, this);
|
||||||
|
} else if (_sendGStr.size()) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_INFILESIZE, (long) _sendGStr.size());
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_READFUNCTION, curlReadFromGString);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_READDATA, this);
|
||||||
|
}
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_UPLOAD, 1L); // cURL now needs this to actually send the email, cf. "http://curl.haxx.se/mail/lib-2013-12/0152.html".
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add SMTP recipient to the `_headers` (which are then set into `CURLOPT_MAIL_RCPT` by the `Curl::smtp`).
|
||||||
|
* NB: Should be used *before* the `Curl::smtp`! */
|
||||||
|
Curl& bcc (const char* to) {
|
||||||
|
if (to) _headers = curl_slist_append (_headers, to);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Uses `CURLOPT_CUSTOMREQUEST` to set the http method.
|
||||||
|
Can be used both before and after the `http` method.\n
|
||||||
|
Example sending a POST request to ElasticSearch: \code
|
||||||
|
glim::Curl curl;
|
||||||
|
curl.send (C2GSTRING (R"({"query":{"match_all":{}},"facets":{"tags":{"terms":{"field":"tags","size":1000}}}})"));
|
||||||
|
curl.method ("POST") .http ("http://127.0.0.1:9200/froples/frople/_search", 120);
|
||||||
|
if (curl.verbose().go().status() != 200) GTHROW ("Error fetching tags: " + std::to_string (curl.status()) + ", " + curl.str());
|
||||||
|
cout << curl.gstr() << endl;
|
||||||
|
\endcode */
|
||||||
|
Curl& method (const char* method) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_CUSTOMREQUEST, method);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setup a handler to process the headers cURL gets from the response.
|
||||||
|
* "The header callback will be called once for each header and only complete header lines are passed on to the callback".\n
|
||||||
|
* See http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTHEADERFUNCTION */
|
||||||
|
Curl& headerListener (std::function<void (const char* header, int len)> listener) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_HEADERFUNCTION, curlWriteHeader);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_WRITEHEADER, this);
|
||||||
|
_headerListener = listener;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setup a handler to get the debug messages generated by cURL.
|
||||||
|
* See http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTDEBUGFUNCTION */
|
||||||
|
Curl& debugListener (std::function<void (curl_infotype type, char* bytes, size_t size)> listener) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_DEBUGFUNCTION, curlDebugCB);
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_DEBUGDATA, this);
|
||||||
|
_debugListener = listener;
|
||||||
|
return verbose (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Setup a handler to get some of the debug messages generated by cURL.
|
||||||
|
Listener gets a formatted text: outbound data is prepended with "> " and inbound with "< ".\n
|
||||||
|
Usage example: \code
|
||||||
|
auto curlDebug = std::make_shared<std::string>();
|
||||||
|
curl->debugListenerF ([curlDebug](const char* bytes, size_t size) {curlDebug->append (bytes, size);});
|
||||||
|
...
|
||||||
|
if (curl->status() != 200) std::cerr << "cURL status != 200; debug follows: " << *curlDebug << std::endl;
|
||||||
|
\endcode
|
||||||
|
See http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTDEBUGFUNCTION
|
||||||
|
@param listener The receiver of the debug information.
|
||||||
|
@param data Whether to pass the data (`CURLINFO_DATA_IN`, `CURLINFO_DATA_OUT`) to the `listener`.
|
||||||
|
*/
|
||||||
|
Curl& debugListenerF (std::function<void (const char* bytes, size_t size)> listener, bool data = false) {
|
||||||
|
return debugListener ([listener/* = std::move (listener)*/,data] (curl_infotype type, char* bytes, size_t size) {
|
||||||
|
GSTRING_ON_STACK (buf, 256);
|
||||||
|
auto prepend = [&](const char* prefix) {
|
||||||
|
buf << prefix; for (char *p = bytes, *end = bytes + size; p < end; ++p) {buf << *p; if (*p == '\n' && p + 2 < end) buf << prefix;}};
|
||||||
|
if (type == CURLINFO_HEADER_IN || (type == CURLINFO_DATA_IN && data)) prepend ("< ");
|
||||||
|
else if (type == CURLINFO_HEADER_OUT || (type == CURLINFO_DATA_OUT && data)) prepend ("> ");
|
||||||
|
listener (buf.c_str(), buf.size());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to print debug information to `CURLOPT_STDERR`.
|
||||||
|
/// Note that when `debugListener` is used, verbose output will go to the listener and not to `CURLOPT_STDERR`.
|
||||||
|
Curl& verbose (bool on = true) {
|
||||||
|
curl_easy_setopt (_curl, CURLOPT_VERBOSE, on ? 1L : 0L);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the buffers and perform the cURL request.
|
||||||
|
Curl& go() {
|
||||||
|
_got.clear();
|
||||||
|
*_errorBuf = 0;
|
||||||
|
if (curl_easy_perform (_curl)) throw PerformError (_errorBuf, __FILE__, __LINE__);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contents of the response.
|
||||||
|
const std::string& str() const {return _got;}
|
||||||
|
/// CString of `str`.
|
||||||
|
const char* c_str() const {return _got.c_str();}
|
||||||
|
/// Returns a gstring "view" into `str`.
|
||||||
|
gstring gstr() const {return gstring (0, (void*) _got.data(), false, _got.size());}
|
||||||
|
|
||||||
|
/// The status of the response (For HTTP it's 200 ok, 404 not found, 500 error, etc).
|
||||||
|
long status() const {
|
||||||
|
long status; CURLcode err = curl_easy_getinfo (_curl, CURLINFO_RESPONSE_CODE, &status);
|
||||||
|
if (err) {
|
||||||
|
GSTRING_ON_STACK (message, 128) << "CURL error " << (int) err << ": " << curl_easy_strerror (err);
|
||||||
|
throw GetinfoError (err, message.str(), __FILE__, __LINE__);
|
||||||
|
}
|
||||||
|
return status;}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Moves the content to be sent into a `glim::gstring` inside `Curl`.
|
||||||
|
* NB: In order to have an effect this method should be used *before* the `http()` and `smtp()` methods. */
|
||||||
|
template<> inline Curl& Curl::send<gstring> (gstring&& text) {
|
||||||
|
_sendStr.clear();
|
||||||
|
_sendGStr = std::move (text);
|
||||||
|
_sent = 0;
|
||||||
|
return *this;}
|
||||||
|
|
||||||
|
inline size_t curlReadFromString (void *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||||
|
Curl* curl = (Curl*) userdata;
|
||||||
|
size_t len = std::min (curl->_sendStr.size() - curl->_sent, size * nmemb);
|
||||||
|
if (len) memcpy (ptr, curl->_sendStr.data() + curl->_sent, len);
|
||||||
|
curl->_sent += len;
|
||||||
|
return len;}
|
||||||
|
|
||||||
|
inline size_t curlReadFromGString (void *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||||
|
Curl* curl = (Curl*) userdata;
|
||||||
|
size_t len = std::min (curl->_sendGStr.size() - curl->_sent, size * nmemb);
|
||||||
|
if (len) memcpy (ptr, curl->_sendGStr.data() + curl->_sent, len);
|
||||||
|
curl->_sent += len;
|
||||||
|
return len;}
|
||||||
|
|
||||||
|
// http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTHEADERFUNCTION
|
||||||
|
inline size_t curlWriteHeader (void *ptr, size_t size, size_t nmemb, void *curlPtr) {
|
||||||
|
Curl* curl = (Curl*) curlPtr;
|
||||||
|
std::function<void (const char* header, int len)>& listener = curl->_headerListener;
|
||||||
|
int len = size * nmemb;
|
||||||
|
if (listener) listener ((const char*) ptr, len);
|
||||||
|
return (size_t) len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTDEBUGFUNCTION
|
||||||
|
inline int curlDebugCB (CURL*, curl_infotype type, char* bytes, size_t size, void* curlPtr) {
|
||||||
|
Curl* curl = (Curl*) curlPtr;
|
||||||
|
auto& listener = curl->_debugListener;
|
||||||
|
if (listener) listener (type, bytes, size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Example: std::string w3 = glim::curl2str ("http://www.w3.org/");
|
||||||
|
inline std::string curl2str (const char* url, int timeoutSec = 20) {
|
||||||
|
try {
|
||||||
|
return glim::Curl().http (url, timeoutSec) .go().str();
|
||||||
|
} catch (const std::exception&) {}
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,237 @@
|
||||||
|
# Doxyfile 1.8.4; http://www.stack.nl/~dimitri/doxygen/manual/config.html
|
||||||
|
DOXYFILE_ENCODING = UTF-8
|
||||||
|
PROJECT_NAME = "libglim"
|
||||||
|
PROJECT_NUMBER = 0.7
|
||||||
|
OUTPUT_DIRECTORY = doc
|
||||||
|
CREATE_SUBDIRS = NO
|
||||||
|
OUTPUT_LANGUAGE = English
|
||||||
|
BRIEF_MEMBER_DESC = YES
|
||||||
|
REPEAT_BRIEF = YES
|
||||||
|
ALWAYS_DETAILED_SEC = NO
|
||||||
|
INLINE_INHERITED_MEMB = NO
|
||||||
|
FULL_PATH_NAMES = NO
|
||||||
|
SHORT_NAMES = NO
|
||||||
|
JAVADOC_AUTOBRIEF = YES
|
||||||
|
QT_AUTOBRIEF = NO
|
||||||
|
MULTILINE_CPP_IS_BRIEF = NO
|
||||||
|
INHERIT_DOCS = YES
|
||||||
|
SEPARATE_MEMBER_PAGES = NO
|
||||||
|
TAB_SIZE = 2
|
||||||
|
OPTIMIZE_OUTPUT_FOR_C = YES
|
||||||
|
OPTIMIZE_OUTPUT_JAVA = NO
|
||||||
|
OPTIMIZE_FOR_FORTRAN = NO
|
||||||
|
OPTIMIZE_OUTPUT_VHDL = NO
|
||||||
|
# http://daringfireball.net/projects/markdown/
|
||||||
|
MARKDOWN_SUPPORT = YES
|
||||||
|
AUTOLINK_SUPPORT = YES
|
||||||
|
BUILTIN_STL_SUPPORT = YES
|
||||||
|
CPP_CLI_SUPPORT = NO
|
||||||
|
SIP_SUPPORT = NO
|
||||||
|
IDL_PROPERTY_SUPPORT = NO
|
||||||
|
DISTRIBUTE_GROUP_DOC = NO
|
||||||
|
SUBGROUPING = YES
|
||||||
|
INLINE_GROUPED_CLASSES = NO
|
||||||
|
INLINE_SIMPLE_STRUCTS = YES
|
||||||
|
TYPEDEF_HIDES_STRUCT = NO
|
||||||
|
LOOKUP_CACHE_SIZE = 0
|
||||||
|
EXTRACT_ALL = NO
|
||||||
|
EXTRACT_PRIVATE = NO
|
||||||
|
EXTRACT_PACKAGE = NO
|
||||||
|
EXTRACT_STATIC = NO
|
||||||
|
EXTRACT_LOCAL_CLASSES = YES
|
||||||
|
EXTRACT_LOCAL_METHODS = NO
|
||||||
|
EXTRACT_ANON_NSPACES = NO
|
||||||
|
HIDE_UNDOC_MEMBERS = YES
|
||||||
|
HIDE_UNDOC_CLASSES = YES
|
||||||
|
HIDE_FRIEND_COMPOUNDS = NO
|
||||||
|
HIDE_IN_BODY_DOCS = NO
|
||||||
|
INTERNAL_DOCS = NO
|
||||||
|
CASE_SENSE_NAMES = YES
|
||||||
|
HIDE_SCOPE_NAMES = NO
|
||||||
|
SHOW_INCLUDE_FILES = YES
|
||||||
|
FORCE_LOCAL_INCLUDES = NO
|
||||||
|
INLINE_INFO = YES
|
||||||
|
SORT_MEMBER_DOCS = YES
|
||||||
|
SORT_BRIEF_DOCS = YES
|
||||||
|
SORT_MEMBERS_CTORS_1ST = NO
|
||||||
|
SORT_GROUP_NAMES = NO
|
||||||
|
SORT_BY_SCOPE_NAME = NO
|
||||||
|
STRICT_PROTO_MATCHING = NO
|
||||||
|
GENERATE_TODOLIST = YES
|
||||||
|
GENERATE_TESTLIST = YES
|
||||||
|
GENERATE_BUGLIST = YES
|
||||||
|
GENERATE_DEPRECATEDLIST= YES
|
||||||
|
MAX_INITIALIZER_LINES = 30
|
||||||
|
SHOW_USED_FILES = YES
|
||||||
|
SHOW_FILES = YES
|
||||||
|
SHOW_NAMESPACES = YES
|
||||||
|
QUIET = NO
|
||||||
|
WARNINGS = YES
|
||||||
|
WARN_IF_UNDOCUMENTED = NO
|
||||||
|
WARN_IF_DOC_ERROR = YES
|
||||||
|
WARN_NO_PARAMDOC = NO
|
||||||
|
WARN_FORMAT = "$file:$line: $text"
|
||||||
|
|
||||||
|
INPUT = ./
|
||||||
|
INPUT_ENCODING = UTF-8
|
||||||
|
FILE_PATTERNS = *.hpp
|
||||||
|
RECURSIVE = NO
|
||||||
|
EXCLUDE =
|
||||||
|
EXCLUDE_SYMLINKS = NO
|
||||||
|
EXCLUDE_PATTERNS =
|
||||||
|
EXCLUDE_SYMBOLS =
|
||||||
|
EXAMPLE_PATH =
|
||||||
|
EXAMPLE_PATTERNS = test_*.cc
|
||||||
|
EXAMPLE_RECURSIVE = NO
|
||||||
|
IMAGE_PATH =
|
||||||
|
FILTER_SOURCE_FILES = NO
|
||||||
|
SOURCE_BROWSER = NO
|
||||||
|
INLINE_SOURCES = NO
|
||||||
|
STRIP_CODE_COMMENTS = NO
|
||||||
|
REFERENCED_BY_RELATION = NO
|
||||||
|
REFERENCES_RELATION = NO
|
||||||
|
REFERENCES_LINK_SOURCE = YES
|
||||||
|
USE_HTAGS = NO
|
||||||
|
VERBATIM_HEADERS = YES
|
||||||
|
CLANG_ASSISTED_PARSING = NO
|
||||||
|
ALPHABETICAL_INDEX = YES
|
||||||
|
COLS_IN_ALPHA_INDEX = 5
|
||||||
|
GENERATE_HTML = YES
|
||||||
|
HTML_OUTPUT = html
|
||||||
|
HTML_FILE_EXTENSION = .html
|
||||||
|
HTML_HEADER =
|
||||||
|
HTML_FOOTER =
|
||||||
|
HTML_STYLESHEET =
|
||||||
|
HTML_EXTRA_STYLESHEET =
|
||||||
|
HTML_EXTRA_FILES =
|
||||||
|
HTML_COLORSTYLE_HUE = 220
|
||||||
|
HTML_COLORSTYLE_SAT = 100
|
||||||
|
HTML_COLORSTYLE_GAMMA = 80
|
||||||
|
HTML_TIMESTAMP = YES
|
||||||
|
HTML_DYNAMIC_SECTIONS = NO
|
||||||
|
HTML_INDEX_NUM_ENTRIES = 100
|
||||||
|
GENERATE_DOCSET = NO
|
||||||
|
DOCSET_FEEDNAME = "Doxygen generated docs"
|
||||||
|
DOCSET_BUNDLE_ID = org.doxygen.Project
|
||||||
|
DOCSET_PUBLISHER_ID = org.doxygen.Publisher
|
||||||
|
DOCSET_PUBLISHER_NAME = Publisher
|
||||||
|
GENERATE_HTMLHELP = NO
|
||||||
|
CHM_FILE =
|
||||||
|
HHC_LOCATION =
|
||||||
|
GENERATE_CHI = NO
|
||||||
|
CHM_INDEX_ENCODING =
|
||||||
|
BINARY_TOC = NO
|
||||||
|
TOC_EXPAND = NO
|
||||||
|
GENERATE_QHP = NO
|
||||||
|
QCH_FILE =
|
||||||
|
QHP_NAMESPACE = org.doxygen.Project
|
||||||
|
QHP_VIRTUAL_FOLDER = doc
|
||||||
|
QHP_CUST_FILTER_NAME =
|
||||||
|
QHP_CUST_FILTER_ATTRS =
|
||||||
|
QHP_SECT_FILTER_ATTRS =
|
||||||
|
QHG_LOCATION =
|
||||||
|
GENERATE_ECLIPSEHELP = NO
|
||||||
|
ECLIPSE_DOC_ID = org.doxygen.Project
|
||||||
|
DISABLE_INDEX = NO
|
||||||
|
GENERATE_TREEVIEW = NO
|
||||||
|
ENUM_VALUES_PER_LINE = 4
|
||||||
|
TREEVIEW_WIDTH = 250
|
||||||
|
EXT_LINKS_IN_WINDOW = NO
|
||||||
|
FORMULA_FONTSIZE = 10
|
||||||
|
FORMULA_TRANSPARENT = YES
|
||||||
|
USE_MATHJAX = NO
|
||||||
|
MATHJAX_FORMAT = HTML-CSS
|
||||||
|
MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest
|
||||||
|
MATHJAX_EXTENSIONS =
|
||||||
|
MATHJAX_CODEFILE =
|
||||||
|
SEARCHENGINE = YES
|
||||||
|
SERVER_BASED_SEARCH = NO
|
||||||
|
EXTERNAL_SEARCH = NO
|
||||||
|
SEARCHENGINE_URL =
|
||||||
|
SEARCHDATA_FILE = searchdata.xml
|
||||||
|
EXTERNAL_SEARCH_ID =
|
||||||
|
EXTRA_SEARCH_MAPPINGS =
|
||||||
|
GENERATE_LATEX = YES
|
||||||
|
LATEX_OUTPUT = latex
|
||||||
|
LATEX_CMD_NAME = latex
|
||||||
|
MAKEINDEX_CMD_NAME = makeindex
|
||||||
|
COMPACT_LATEX = NO
|
||||||
|
PAPER_TYPE = a4
|
||||||
|
EXTRA_PACKAGES =
|
||||||
|
LATEX_HEADER =
|
||||||
|
LATEX_FOOTER =
|
||||||
|
LATEX_EXTRA_FILES =
|
||||||
|
PDF_HYPERLINKS = YES
|
||||||
|
USE_PDFLATEX = NO
|
||||||
|
LATEX_BATCHMODE = NO
|
||||||
|
LATEX_HIDE_INDICES = NO
|
||||||
|
LATEX_SOURCE_CODE = NO
|
||||||
|
LATEX_BIB_STYLE = plain
|
||||||
|
GENERATE_RTF = NO
|
||||||
|
RTF_OUTPUT = rtf
|
||||||
|
COMPACT_RTF = NO
|
||||||
|
RTF_HYPERLINKS = NO
|
||||||
|
RTF_STYLESHEET_FILE =
|
||||||
|
RTF_EXTENSIONS_FILE =
|
||||||
|
GENERATE_MAN = NO
|
||||||
|
MAN_OUTPUT = man
|
||||||
|
MAN_EXTENSION = .3
|
||||||
|
MAN_LINKS = NO
|
||||||
|
GENERATE_XML = NO
|
||||||
|
XML_OUTPUT = xml
|
||||||
|
XML_SCHEMA =
|
||||||
|
XML_DTD =
|
||||||
|
XML_PROGRAMLISTING = YES
|
||||||
|
GENERATE_DOCBOOK = NO
|
||||||
|
DOCBOOK_OUTPUT = docbook
|
||||||
|
GENERATE_AUTOGEN_DEF = NO
|
||||||
|
GENERATE_PERLMOD = NO
|
||||||
|
PERLMOD_LATEX = NO
|
||||||
|
PERLMOD_PRETTY = YES
|
||||||
|
PERLMOD_MAKEVAR_PREFIX =
|
||||||
|
ENABLE_PREPROCESSING = YES
|
||||||
|
MACRO_EXPANSION = NO
|
||||||
|
EXPAND_ONLY_PREDEF = NO
|
||||||
|
SEARCH_INCLUDES = YES
|
||||||
|
INCLUDE_PATH =
|
||||||
|
INCLUDE_FILE_PATTERNS =
|
||||||
|
PREDEFINED =
|
||||||
|
EXPAND_AS_DEFINED =
|
||||||
|
SKIP_FUNCTION_MACROS = YES
|
||||||
|
TAGFILES =
|
||||||
|
GENERATE_TAGFILE =
|
||||||
|
ALLEXTERNALS = NO
|
||||||
|
EXTERNAL_GROUPS = YES
|
||||||
|
EXTERNAL_PAGES = YES
|
||||||
|
PERL_PATH = /usr/bin/perl
|
||||||
|
CLASS_DIAGRAMS = YES
|
||||||
|
MSCGEN_PATH =
|
||||||
|
HIDE_UNDOC_RELATIONS = YES
|
||||||
|
HAVE_DOT = NO
|
||||||
|
DOT_NUM_THREADS = 0
|
||||||
|
DOT_FONTNAME = Helvetica
|
||||||
|
DOT_FONTSIZE = 10
|
||||||
|
DOT_FONTPATH =
|
||||||
|
CLASS_GRAPH = YES
|
||||||
|
COLLABORATION_GRAPH = YES
|
||||||
|
GROUP_GRAPHS = YES
|
||||||
|
UML_LOOK = NO
|
||||||
|
UML_LIMIT_NUM_FIELDS = 10
|
||||||
|
TEMPLATE_RELATIONS = NO
|
||||||
|
INCLUDE_GRAPH = YES
|
||||||
|
INCLUDED_BY_GRAPH = YES
|
||||||
|
CALL_GRAPH = NO
|
||||||
|
CALLER_GRAPH = NO
|
||||||
|
GRAPHICAL_HIERARCHY = YES
|
||||||
|
DIRECTORY_GRAPH = YES
|
||||||
|
DOT_IMAGE_FORMAT = png
|
||||||
|
INTERACTIVE_SVG = NO
|
||||||
|
DOT_PATH =
|
||||||
|
DOTFILE_DIRS =
|
||||||
|
MSCFILE_DIRS =
|
||||||
|
DOT_GRAPH_MAX_NODES = 50
|
||||||
|
MAX_DOT_GRAPH_DEPTH = 0
|
||||||
|
DOT_TRANSPARENT = NO
|
||||||
|
DOT_MULTI_TARGETS = NO
|
||||||
|
GENERATE_LEGEND = YES
|
||||||
|
DOT_CLEANUP = YES
|
|
@ -0,0 +1,259 @@
|
||||||
|
#ifndef _GLIM_EXCEPTION_HPP_INCLUDED
|
||||||
|
#define _GLIM_EXCEPTION_HPP_INCLUDED
|
||||||
|
|
||||||
|
/// \file
|
||||||
|
/// Exceptions with configurable behaviour.
|
||||||
|
/// Requires `thread_local` support introduced in [gcc-4.8](http://gcc.gnu.org/gcc-4.8/changes.html)
|
||||||
|
/// (`__thread` is not reliable with GCC 4.7.2 across shared libraries).
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h> // free
|
||||||
|
#include <unistd.h> // write
|
||||||
|
|
||||||
|
/// Throws `::glim::Exception` passing the current file and line into constructor.
|
||||||
|
#define GTHROW(message) throw ::glim::Exception (message, __FILE__, __LINE__)
|
||||||
|
/// Throws a `::glim::Exception` derived exception `name` passing the current file and line into constructor.
|
||||||
|
#define GNTHROW(name, message) throw name (message, __FILE__, __LINE__)
|
||||||
|
/// Helps defining new `::glim::Exception`-based exceptions.
|
||||||
|
/// Named exceptions might be useful in a debugger.
|
||||||
|
#define G_DEFINE_EXCEPTION(name) \
|
||||||
|
struct name: public ::glim::Exception { \
|
||||||
|
name (const ::std::string& message, const char* file, int line): ::glim::Exception (message, file, line) {} \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround to compile under GCC 4.7.
|
||||||
|
#if defined (__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ == 7 && !defined (thread_local)
|
||||||
|
# define thread_local __thread
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
// Ideas:
|
||||||
|
// RAII control via thread-local integer (with bits): option to capture stack trace (printed on "what()")
|
||||||
|
// see http://stacktrace.svn.sourceforge.net/viewvc/stacktrace/stacktrace/call_stack_gcc.cpp?revision=40&view=markup
|
||||||
|
// A handler to log exception with VALGRIND (with optional trace)
|
||||||
|
// A handler to log thread id and *pause* the thread in exception constructor (user can attach GDB and investigate)
|
||||||
|
// (or we might call an empty function: "I once used something similar,
|
||||||
|
// but with an empty function debug_breakpoint. When debugging, I simply entered "bre debug_breakpoint"
|
||||||
|
// at the gdb prompt - no asembler needed (compile debug_breakpoint in a separate compilation unit to avoid having the call optimized away)."
|
||||||
|
// - http://stackoverflow.com/a/4720403/257568)
|
||||||
|
// A handler to call a debugger? (see: http://stackoverflow.com/a/4732119/257568)
|
||||||
|
|
||||||
|
// todo: Try a helper which uses cairo's backtrace-symbols.c
|
||||||
|
// http://code.ohloh.net/file?fid=zUOUdEl-Id-ijyPOmCkVnBJt2d8&cid=zGpizbyIjEw&s=addr2line&browser=Default#L7
|
||||||
|
|
||||||
|
// todo: Try a helper which uses cairo's lookup-symbol.c
|
||||||
|
// http://code.ohloh.net/file?fid=Je2jZqsOxge_SvWVrvywn2I0TIs&cid=zGpizbyIjEw&s=addr2line&browser=Default#L0
|
||||||
|
|
||||||
|
// todo: A helper converting backtrace to addr2line invocation, e.g.
|
||||||
|
// bin/test_exception() [0x4020cc];bin/test_exception(__cxa_throw+0x47) [0x402277];bin/test_exception() [0x401c06];/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xfd) [0x57f0ead];bin/test_exception() [0x401fd1];
|
||||||
|
// should be converted to
|
||||||
|
// addr2line -pifCa -e bin/test_exception 0x4020cc 0x402277 0x401c06 0x57f0ead 0x401fd1
|
||||||
|
//
|
||||||
|
// The helper should read the shared library addresses from /proc/.../map and generate separate addr2line invocations
|
||||||
|
// for groups of addresses inside the same shared library.
|
||||||
|
// => dladdr instead of /proc/../map; http://stackoverflow.com/a/2606152/257568
|
||||||
|
//
|
||||||
|
// Shared libraries (http://stackoverflow.com/a/7557756/257568).
|
||||||
|
// Example, backtrace: /usr/local/lib/libfrople.so(_ZN5mongo14BSONObjBuilder8appendAsERKNS_11BSONElementERKNS_10StringDataE+0x1ca) [0x2aef5b45eb8a]
|
||||||
|
// cat /proc/23630/maps | grep libfrople
|
||||||
|
// -> 2aef5b363000-2aef5b53e000
|
||||||
|
// 0x2aef5b45eb8a - 2aef5b363000 = FBB8A
|
||||||
|
// addr2line -pifCa -e /usr/local/lib/libfrople.so 0xFBB8A
|
||||||
|
//
|
||||||
|
// cat /proc/`pidof FropleAndImg2`/maps | grep libfrople
|
||||||
|
// addr2line -pifCa -e /usr/local/lib/libfrople.so `perl -e 'printf ("%x", 0x2aef5b45eb8a - 0x2aef5b363000)'`
|
||||||
|
|
||||||
|
inline void captureBacktrace (void* stdStringPtr);
|
||||||
|
|
||||||
|
typedef void (*exception_handler_fn)(void*);
|
||||||
|
|
||||||
|
/// Exception with file and line information and optional stack trace capture.
|
||||||
|
/// Requires `thread_local` support ([gcc-4.8](http://gcc.gnu.org/gcc-4.8/changes.html)).
|
||||||
|
class Exception: public std::runtime_error {
|
||||||
|
protected:
|
||||||
|
const char* _file; int32_t _line;
|
||||||
|
std::string _what;
|
||||||
|
uint32_t _options;
|
||||||
|
|
||||||
|
/// Append [{file}:{line}] into `buf`.
|
||||||
|
void appendLine (std::string& buf) const {
|
||||||
|
if (_file || _line > 0) {
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << '[';
|
||||||
|
if (_file) oss << _file;
|
||||||
|
if (_line >= 0) oss << ':' << _line;
|
||||||
|
oss << "] ";
|
||||||
|
buf.append (oss.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a stack trace to `_what`.
|
||||||
|
void capture() {
|
||||||
|
if (_options & RENDEZVOUS) rendezvous();
|
||||||
|
if (_options & CAPTURE_TRACE) {
|
||||||
|
appendLine (_what);
|
||||||
|
_what += "[at ";
|
||||||
|
captureBacktrace (&_what);
|
||||||
|
_what.append ("] ");
|
||||||
|
_what += std::runtime_error::what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** The reference to the thread-local options. */
|
||||||
|
inline static uint32_t& options() {
|
||||||
|
static thread_local uint32_t EXCEPTION_OPTIONS = 0;
|
||||||
|
return EXCEPTION_OPTIONS;
|
||||||
|
}
|
||||||
|
enum Options: uint32_t {
|
||||||
|
PLAIN_WHAT = 1, ///< Pass `what` as is, do not add any information to it.
|
||||||
|
HANDLE_ALL = 1 << 1, ///< Run the custom handler from `__cxa_throw`.
|
||||||
|
CAPTURE_TRACE = 1 << 2, ///< Append a stack trace into the `Exception::_what` (with the help of the `captureBacktrace`).
|
||||||
|
RENDEZVOUS = 1 << 3 ///< Call the rendezvous function in `throw` and in `what`, so that the GDB can catch it (break glim::Exception::rendezvous).
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The pointer to the thread-local exception handler. */
|
||||||
|
inline static exception_handler_fn* handler() {
|
||||||
|
static thread_local exception_handler_fn EXCEPTION_HANDLER = nullptr;
|
||||||
|
return &EXCEPTION_HANDLER;
|
||||||
|
}
|
||||||
|
/** The pointer to the thread-local argument for the exception handler. */
|
||||||
|
inline static void** handlerArg() {
|
||||||
|
static thread_local void* EXCEPTION_HANDLER_ARG = nullptr;
|
||||||
|
return &EXCEPTION_HANDLER_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoked when the `RENDEZVOUS` option is set in order to help the debugger catch the exception (break glim::Exception::rendezvous).
|
||||||
|
static void rendezvous() __attribute__((noinline)) {
|
||||||
|
asm (""); // Prevents the function from being optimized away.
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception (const std::string& message):
|
||||||
|
std::runtime_error (message), _file (0), _line (-1), _options (options()) {
|
||||||
|
capture();}
|
||||||
|
Exception (const std::string& message, const char* file, int32_t line):
|
||||||
|
std::runtime_error (message), _file (file), _line (line), _options (options()) {
|
||||||
|
capture();}
|
||||||
|
~Exception() throw() {}
|
||||||
|
virtual const char* what() const throw() {
|
||||||
|
if (_options & RENDEZVOUS) rendezvous();
|
||||||
|
if (_options & PLAIN_WHAT) return std::runtime_error::what();
|
||||||
|
std::string& buf = const_cast<std::string&> (_what);
|
||||||
|
if (buf.empty()) {
|
||||||
|
appendLine (buf);
|
||||||
|
buf.append (std::runtime_error::what());
|
||||||
|
}
|
||||||
|
return buf.c_str();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// RAII control of thrown `Exception`s.
|
||||||
|
/// Example: \code
|
||||||
|
/// glim::ExceptionControl trace (glim::Exception::Options::CAPTURE_TRACE);
|
||||||
|
/// \endcode
|
||||||
|
/// Modifies the `Exception` options via a thread-local variable and restores them back upon destruction.\n
|
||||||
|
/// Currently uses http://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Thread_002dLocal.html
|
||||||
|
/// (might use C++11 `thread_local` in the future).
|
||||||
|
class ExceptionControl {
|
||||||
|
protected:
|
||||||
|
uint32_t _savedOptions;
|
||||||
|
public:
|
||||||
|
ExceptionControl (uint32_t newOptions) {
|
||||||
|
uint32_t& options = Exception::options();
|
||||||
|
_savedOptions = options;
|
||||||
|
options = newOptions;
|
||||||
|
}
|
||||||
|
~ExceptionControl() {
|
||||||
|
Exception::options() = _savedOptions;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExceptionHandler {
|
||||||
|
protected:
|
||||||
|
uint32_t _savedOptions;
|
||||||
|
exception_handler_fn _savedHandler;
|
||||||
|
void* _savedHandlerArg;
|
||||||
|
public:
|
||||||
|
ExceptionHandler (uint32_t newOptions, exception_handler_fn handler, void* handlerArg) {
|
||||||
|
uint32_t& options = Exception::options(); _savedOptions = options; options = newOptions;
|
||||||
|
exception_handler_fn* handler_ = Exception::handler(); _savedHandler = *handler_; *handler_ = handler;
|
||||||
|
void** handlerArg_ = Exception::handlerArg(); _savedHandlerArg = *handlerArg_; *handlerArg_ = handlerArg;
|
||||||
|
}
|
||||||
|
~ExceptionHandler() {
|
||||||
|
Exception::options() = _savedOptions;
|
||||||
|
*Exception::handler() = _savedHandler;
|
||||||
|
*Exception::handlerArg() = _savedHandlerArg;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace glim
|
||||||
|
|
||||||
|
#if defined(__GNUC__) && (defined (__linux__) || defined (_SYSTYPE_BSD))
|
||||||
|
# include <execinfo.h> // backtrace; http://www.gnu.org/software/libc/manual/html_node/Backtraces.html
|
||||||
|
# define _GLIM_USE_EXECINFO
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
/** If `stdStringPtr` is not null then backtrace is saved there (must point to an std::string instance),
|
||||||
|
* otherwise printed to write(2). */
|
||||||
|
void captureBacktrace (void* stdStringPtr) {
|
||||||
|
#ifdef _GLIM_USE_EXECINFO
|
||||||
|
const int arraySize = 10; void *array[arraySize];
|
||||||
|
int got = ::backtrace (array, arraySize);
|
||||||
|
if (stdStringPtr) {
|
||||||
|
std::string* out = (std::string*) stdStringPtr;
|
||||||
|
char **strings = ::backtrace_symbols (array, got);
|
||||||
|
for (int tn = 0; tn < got; ++tn) {out->append (strings[tn]); out->append (1, ';');}
|
||||||
|
::free (strings);
|
||||||
|
} else ::backtrace_symbols_fd (array, got, 2);
|
||||||
|
#else
|
||||||
|
# warning captureBacktrace: I do not know how to capture backtrace there. Patches welcome.
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace glim
|
||||||
|
|
||||||
|
#endif // _GLIM_EXCEPTION_HPP_INCLUDED
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special handler for ALL exceptions. Usage:
|
||||||
|
* 1) In the `main` module inject this code with:
|
||||||
|
* #define _GLIM_ALL_EXCEPTIONS_CODE
|
||||||
|
* #include <glim/exception.hpp>
|
||||||
|
* 2) Link with "-ldl" (for `dlsym`).
|
||||||
|
* 3) Use the ExceptionHandler to enable special behaviour in the current thread:
|
||||||
|
* glim::ExceptionHandler traceExceptions (glim::Exception::Options::HANDLE_ALL, glim::captureBacktrace, nullptr);
|
||||||
|
*
|
||||||
|
* About handing all exceptions see:
|
||||||
|
* http://stackoverflow.com/a/11674810/257568
|
||||||
|
* http://blog.sjinks.pro/c-cpp/969-track-uncaught-exceptions/
|
||||||
|
*/
|
||||||
|
#ifdef _GLIM_ALL_EXCEPTIONS_CODE
|
||||||
|
|
||||||
|
#include <dlfcn.h> // dlsym
|
||||||
|
|
||||||
|
typedef void(*cxa_throw_type)(void*, void*, void(*)(void*)); // Tested with GCC 4.7.
|
||||||
|
static cxa_throw_type NATIVE_CXA_THROW = 0;
|
||||||
|
|
||||||
|
extern "C" void __cxa_throw (void* thrown_exception, void* tinfo, void (*dest)(void*)) {
|
||||||
|
if (!NATIVE_CXA_THROW) NATIVE_CXA_THROW = reinterpret_cast<cxa_throw_type> (::dlsym (RTLD_NEXT, "__cxa_throw"));
|
||||||
|
if (!NATIVE_CXA_THROW) ::std::terminate();
|
||||||
|
|
||||||
|
using namespace glim;
|
||||||
|
uint32_t options = Exception::options();
|
||||||
|
if (options & Exception::RENDEZVOUS) Exception::rendezvous();
|
||||||
|
if (options & Exception::HANDLE_ALL) {
|
||||||
|
exception_handler_fn handler = *Exception::handler();
|
||||||
|
if (handler) handler (*Exception::handlerArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
NATIVE_CXA_THROW (thrown_exception, tinfo, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef _GLIM_ALL_EXCEPTIONS_CODE
|
||||||
|
#endif // _GLIM_ALL_EXCEPTIONS_CODE
|
|
@ -0,0 +1,578 @@
|
||||||
|
#ifndef _GSTRING_INCLUDED
|
||||||
|
#define _GSTRING_INCLUDED
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A C++ char string.\n
|
||||||
|
* Can reuse (stack-allocated) buffers.\n
|
||||||
|
* Can create zero-copy views.
|
||||||
|
* @code
|
||||||
|
Copyright 2012 Kozarezov Artem Aleksandrovich
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
* @endcode
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h> // malloc, realloc, free
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h> // memcpy, memmem
|
||||||
|
#include <stdio.h> // snprintf
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <iostream>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
|
#include "exception.hpp"
|
||||||
|
|
||||||
|
/// Make a read-only gstring from a C string: `const gstring foo = C2GSTRING("foo")`.
|
||||||
|
#define C2GSTRING(CSTR) ::glim::gstring (::glim::gstring::ReferenceConstructor(), CSTR, sizeof (CSTR) - 1, true)
|
||||||
|
/// Usage: GSTRING_ON_STACK (buf, 64) << "foo" << "bar";
|
||||||
|
#define GSTRING_ON_STACK(NAME, SIZE) char NAME##Buf[SIZE]; ::glim::gstring NAME (SIZE, NAME##Buf, false, 0); NAME.self()
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on: C++ version 0.4 char* style "itoa": Written by Lukás Chmela, http://www.strudel.org.uk/itoa/ (GPLv3).
|
||||||
|
* Returns a pointer to the end of the string.
|
||||||
|
* NB about `inline`: http://stackoverflow.com/a/1759575/257568
|
||||||
|
* @param base Maximum is 36 (see http://en.wikipedia.org/wiki/Base_36).
|
||||||
|
*/
|
||||||
|
inline char* itoa (char* ptr, int64_t value, const int base = 10) {
|
||||||
|
// check that the base is valid
|
||||||
|
if (base < 2 || base > 36) {*ptr = '\0'; return ptr;}
|
||||||
|
|
||||||
|
char *ptr1 = ptr;
|
||||||
|
int64_t tmp_value;
|
||||||
|
|
||||||
|
do {
|
||||||
|
tmp_value = value;
|
||||||
|
value /= base;
|
||||||
|
*ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz" [35 + (tmp_value - value * base)];
|
||||||
|
} while (value);
|
||||||
|
|
||||||
|
// Apply negative sign
|
||||||
|
if (tmp_value < 0) *ptr++ = '-';
|
||||||
|
char* end = ptr;
|
||||||
|
*ptr-- = '\0';
|
||||||
|
char tmp_char;
|
||||||
|
while (ptr1 < ptr) {
|
||||||
|
tmp_char = *ptr;
|
||||||
|
*ptr--= *ptr1;
|
||||||
|
*ptr1++ = tmp_char;
|
||||||
|
}
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
|
||||||
|
class gstring_stream;
|
||||||
|
|
||||||
|
class gstring {
|
||||||
|
enum Flags {
|
||||||
|
FREE_FLAG = 0x80000000, // 1st bit; `_buf` needs `free`ing
|
||||||
|
FREE_OFFSET = 31,
|
||||||
|
REF_FLAG = 0x40000000, // 2nd bit; `_buf` has an extended life-time (such as C string literals) and can be shared (passed by reference)
|
||||||
|
REF_OFFSET = 30,
|
||||||
|
CAPACITY_MASK = 0x3F000000, // 3..8 bits; `_buf` size is 2^this
|
||||||
|
CAPACITY_OFFSET = 24,
|
||||||
|
LENGTH_MASK = 0x00FFFFFF, // 9th bit; allocated capacity
|
||||||
|
};
|
||||||
|
uint32_t _meta;
|
||||||
|
public:
|
||||||
|
void* _buf;
|
||||||
|
public:
|
||||||
|
constexpr gstring() noexcept: _meta (0), _buf (nullptr) {}
|
||||||
|
/**
|
||||||
|
* Reuse `buf` of size `bufSize`.
|
||||||
|
* To fully use `buf` the `bufSize` should be the power of two.
|
||||||
|
* @param bufSize The size of the memory allocated to `buf`.
|
||||||
|
* @param buf The memory region to be reused.
|
||||||
|
* @param free Whether the `buf` should be `free`d on resize or gstring destruction.
|
||||||
|
* @param length String length inside the `buf`.
|
||||||
|
* @param ref If true then the `buf` isn't copied by gstring's copy constructors.
|
||||||
|
* This is useful for wrapping C string literals.
|
||||||
|
*/
|
||||||
|
explicit gstring (uint32_t bufSize, void* buf, bool free, uint32_t length, bool ref = false) noexcept {
|
||||||
|
uint32_t power = 0; while (((uint32_t) 1 << (power + 1)) <= bufSize) ++power;
|
||||||
|
_meta = ((uint32_t) free << FREE_OFFSET) |
|
||||||
|
((uint32_t) ref << REF_OFFSET) |
|
||||||
|
(power << CAPACITY_OFFSET) |
|
||||||
|
(length & LENGTH_MASK);
|
||||||
|
_buf = buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReferenceConstructor {};
|
||||||
|
/// Make a view to the given cstring.
|
||||||
|
/// @param buf The memory region to be reused.
|
||||||
|
/// @param length String length inside the `buf`.
|
||||||
|
/// @param ref If true then the `buf` isn't copied by gstring's copy constructors.
|
||||||
|
/// This is useful for wrapping C string literals.
|
||||||
|
explicit constexpr gstring (ReferenceConstructor, const char* buf, uint32_t length, bool ref = false) noexcept:
|
||||||
|
_meta (((uint32_t) ref << REF_OFFSET) | (length & LENGTH_MASK)), _buf ((void*) buf) {}
|
||||||
|
|
||||||
|
/// Copy the characters into `gstring`.
|
||||||
|
gstring (const char* chars): _meta (0), _buf (nullptr) {
|
||||||
|
if (chars && *chars) {
|
||||||
|
size_t length = ::strlen (chars);
|
||||||
|
_buf = ::malloc (length);
|
||||||
|
::memcpy (_buf, chars, length);
|
||||||
|
_meta = (uint32_t) FREE_FLAG |
|
||||||
|
(length & LENGTH_MASK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy the characters into `gstring`.
|
||||||
|
gstring (const char* chars, size_t length) {
|
||||||
|
if (length != 0) {
|
||||||
|
_buf = ::malloc (length);
|
||||||
|
::memcpy (_buf, chars, length);
|
||||||
|
} else _buf = nullptr;
|
||||||
|
_meta = (uint32_t) FREE_FLAG |
|
||||||
|
(length & LENGTH_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy into `gstring`.
|
||||||
|
gstring (const std::string& str): _meta (0), _buf (nullptr) {
|
||||||
|
if (!str.empty()) {
|
||||||
|
_buf = ::malloc (str.length());
|
||||||
|
::memcpy (_buf, str.data(), str.length());
|
||||||
|
_meta = (uint32_t) FREE_FLAG |
|
||||||
|
(str.length() & LENGTH_MASK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `gstr` is `copiedByReference` then make a shallow copy of it,
|
||||||
|
/// otherwise copy `gstr` contents into a `malloc`ed buffer.
|
||||||
|
gstring (const gstring& gstr) {
|
||||||
|
uint32_t glen = gstr.length();
|
||||||
|
if (glen != 0) {
|
||||||
|
if (gstr.copiedByReference()) {
|
||||||
|
_meta = gstr._meta; _buf = gstr._buf;
|
||||||
|
} else {
|
||||||
|
_buf = ::malloc (glen);
|
||||||
|
if (!_buf) GTHROW ("!malloc");
|
||||||
|
::memcpy (_buf, gstr._buf, glen);
|
||||||
|
_meta = (uint32_t) FREE_FLAG |
|
||||||
|
(glen & LENGTH_MASK);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_meta = 0; _buf = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gstring (gstring&& gstr) noexcept: _meta (gstr._meta), _buf (gstr._buf) {
|
||||||
|
gstr._meta = 0; gstr._buf = nullptr;
|
||||||
|
}
|
||||||
|
gstring& operator = (const gstring& gstr) {
|
||||||
|
// cf. http://stackoverflow.com/questions/9322174/move-assignment-operator-and-if-this-rhs
|
||||||
|
if (this != &gstr) {
|
||||||
|
uint32_t glen = gstr.length();
|
||||||
|
uint32_t power = 0;
|
||||||
|
uint32_t capacity = this->capacity();
|
||||||
|
if (glen <= capacity && capacity > 1) { // `capacity <= 1` means there is no _buf.
|
||||||
|
// We reuse existing buffer. Keep capacity info.
|
||||||
|
power = (_meta & CAPACITY_MASK) >> CAPACITY_OFFSET;
|
||||||
|
} else {
|
||||||
|
if (_buf != nullptr && needsFreeing()) ::free (_buf);
|
||||||
|
if (gstr.copiedByReference()) {
|
||||||
|
_meta = gstr._meta; _buf = gstr._buf;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
_buf = ::malloc (glen);
|
||||||
|
if (_buf == nullptr) GTHROW ("malloc failed");
|
||||||
|
}
|
||||||
|
::memcpy (_buf, gstr._buf, glen);
|
||||||
|
_meta = (uint32_t) FREE_FLAG |
|
||||||
|
(power << CAPACITY_OFFSET) |
|
||||||
|
(glen & LENGTH_MASK);
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
gstring& operator = (gstring&& gstr) noexcept {
|
||||||
|
assert (this != &gstr);
|
||||||
|
if (_buf != nullptr && needsFreeing()) free (_buf);
|
||||||
|
_meta = gstr._meta; _buf = gstr._buf;
|
||||||
|
gstr._meta = 0; gstr._buf = nullptr;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a copy of the string.
|
||||||
|
gstring clone() const {return gstring (data(), length());}
|
||||||
|
/// If the gstring's buffer is not owned then copy the bytes into the owned one.
|
||||||
|
/// Useful for turning a stack-allocated gstring into a heap-allocated gstring.
|
||||||
|
gstring& owned() {if (!needsFreeing()) *this = gstring (data(), length()); return *this;}
|
||||||
|
/** Returns a reference to the gstring: when the reference is copied the internal buffer is not copied but referenced (shallow copy).\n
|
||||||
|
* This method should only be used if it is know that the life-time of the reference and its copies is less than the life-time of the buffer. */
|
||||||
|
gstring ref() const noexcept {return gstring (0, _buf, false, length(), true);}
|
||||||
|
|
||||||
|
bool needsFreeing() const noexcept {return _meta & FREE_FLAG;}
|
||||||
|
bool copiedByReference() const noexcept {return _meta & REF_FLAG;}
|
||||||
|
/// Current buffer capacity (memory allocated to the string). Returns 1 if no memory allocated.
|
||||||
|
uint32_t capacity() const noexcept {return 1 << ((_meta & CAPACITY_MASK) >> CAPACITY_OFFSET);}
|
||||||
|
uint32_t length() const noexcept {return _meta & LENGTH_MASK;}
|
||||||
|
size_t size() const noexcept {return _meta & LENGTH_MASK;}
|
||||||
|
bool empty() const noexcept {return (_meta & LENGTH_MASK) == 0;}
|
||||||
|
std::string str() const {size_t len = size(); return len ? std::string ((const char*) _buf, len) : std::string();}
|
||||||
|
/// NB: might move the string to a new buffer.
|
||||||
|
const char* c_str() const {
|
||||||
|
uint32_t len = length(); if (len == 0) return "";
|
||||||
|
uint32_t cap = capacity();
|
||||||
|
// c_str should work even for const gstring's, otherwise it's too much of a pain.
|
||||||
|
if (cap < len + 1) const_cast<gstring*> (this) ->reserve (len + 1);
|
||||||
|
char* buf = (char*) _buf; buf[len] = 0; return buf;
|
||||||
|
}
|
||||||
|
bool equals (const char* cstr) const noexcept {
|
||||||
|
const char* cstr_; uint32_t clen_;
|
||||||
|
if (cstr != nullptr) {cstr_ = cstr; clen_ = strlen (cstr);} else {cstr_ = ""; clen_ = 0;}
|
||||||
|
const uint32_t len = length();
|
||||||
|
if (len != clen_) return false;
|
||||||
|
const char* gstr_ = _buf != nullptr ? (const char*) _buf : "";
|
||||||
|
return memcmp (gstr_, cstr_, len) == 0;
|
||||||
|
}
|
||||||
|
bool equals (const gstring& gs) const noexcept {
|
||||||
|
uint32_t llen = length(), olen = gs.length();
|
||||||
|
if (llen != olen) return false;
|
||||||
|
return memcmp ((const char*) _buf, (const char*) gs._buf, llen) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char& operator[] (unsigned index) noexcept {return ((char*)_buf)[index];}
|
||||||
|
const char& operator[] (unsigned index) const noexcept {return ((const char*)_buf)[index];}
|
||||||
|
|
||||||
|
/// Access `_buf` as `char*`. `_buf` might be nullptr.
|
||||||
|
char* data() noexcept {return (char*)_buf;}
|
||||||
|
const char* data() const noexcept {return (const char*)_buf;}
|
||||||
|
|
||||||
|
char* endp() noexcept {return (char*)_buf + length();}
|
||||||
|
const char* endp() const noexcept {return (const char*)_buf + length();}
|
||||||
|
|
||||||
|
gstring view (uint32_t pos, int32_t count = -1) noexcept {
|
||||||
|
return gstring (0, data() + pos, false, count >= 0 ? count : length() - pos, copiedByReference());}
|
||||||
|
const gstring view (uint32_t pos, int32_t count = -1) const noexcept {
|
||||||
|
return gstring (0, (void*)(data() + pos), false, count >= 0 ? count : length() - pos, copiedByReference());}
|
||||||
|
|
||||||
|
// http://en.cppreference.com/w/cpp/concept/Iterator
|
||||||
|
template<typename CT> struct iterator_t: public std::iterator<std::random_access_iterator_tag, CT, int32_t> {
|
||||||
|
CT* _ptr;
|
||||||
|
iterator_t () noexcept: _ptr (nullptr) {}
|
||||||
|
iterator_t (CT* ptr) noexcept: _ptr (ptr) {}
|
||||||
|
iterator_t (const iterator_t<CT>& it) noexcept: _ptr (it._ptr) {}
|
||||||
|
|
||||||
|
CT& operator*() const noexcept {return *_ptr;}
|
||||||
|
CT* operator->() const noexcept {return _ptr;}
|
||||||
|
CT& operator[](int32_t ofs) const noexcept {return _ptr[ofs];}
|
||||||
|
|
||||||
|
iterator_t<CT>& operator++() noexcept {++_ptr; return *this;}
|
||||||
|
iterator_t<CT> operator++(int) noexcept {return iterator_t<CT> (_ptr++);};
|
||||||
|
iterator_t<CT>& operator--() noexcept {--_ptr; return *this;}
|
||||||
|
iterator_t<CT> operator--(int) noexcept {return iterator_t<CT> (_ptr--);};
|
||||||
|
bool operator == (const iterator_t<CT>& i2) const noexcept {return _ptr == i2._ptr;}
|
||||||
|
bool operator != (const iterator_t<CT>& i2) const noexcept {return _ptr != i2._ptr;}
|
||||||
|
bool operator < (const iterator_t<CT>& i2) const noexcept {return _ptr < i2._ptr;}
|
||||||
|
bool operator > (const iterator_t<CT>& i2) const noexcept {return _ptr > i2._ptr;}
|
||||||
|
bool operator <= (const iterator_t<CT>& i2) const noexcept {return _ptr <= i2._ptr;}
|
||||||
|
bool operator >= (const iterator_t<CT>& i2) const noexcept {return _ptr >= i2._ptr;}
|
||||||
|
iterator_t<CT> operator + (int32_t ofs) const noexcept {return iterator (_ptr + ofs);}
|
||||||
|
iterator_t<CT>& operator += (int32_t ofs) noexcept {_ptr += ofs; return *this;}
|
||||||
|
iterator_t<CT> operator - (int32_t ofs) const noexcept {return iterator (_ptr - ofs);}
|
||||||
|
iterator_t<CT>& operator -= (int32_t ofs) noexcept {_ptr -= ofs; return *this;}
|
||||||
|
};
|
||||||
|
// http://en.cppreference.com/w/cpp/concept/Container
|
||||||
|
typedef char value_type;
|
||||||
|
typedef char& reference;
|
||||||
|
typedef const char& const_reference;
|
||||||
|
typedef uint32_t size_type;
|
||||||
|
typedef int32_t difference_type;
|
||||||
|
typedef iterator_t<char> iterator;
|
||||||
|
typedef iterator_t<const char> const_iterator;
|
||||||
|
iterator begin() noexcept {return iterator ((char*) _buf);}
|
||||||
|
const_iterator begin() const noexcept {return const_iterator ((char*) _buf);}
|
||||||
|
iterator end() noexcept {return iterator ((char*) _buf + size());}
|
||||||
|
const_iterator end() const noexcept {return const_iterator ((char*) _buf + size());}
|
||||||
|
const_iterator cbegin() const noexcept {return const_iterator ((char*) _buf);}
|
||||||
|
const_iterator cend() const noexcept {return const_iterator ((char*) _buf + size());}
|
||||||
|
|
||||||
|
/** Returns -1 if not found. */
|
||||||
|
int32_t find (const char* str, int32_t pos, int32_t count) const noexcept {
|
||||||
|
const int32_t hlen = (int32_t) length() - pos;
|
||||||
|
if (hlen <= 0) return -1;
|
||||||
|
char* haystack = (char*) _buf + pos;
|
||||||
|
void* mret = memmem (haystack, hlen, str, count);
|
||||||
|
if (mret == 0) return -1;
|
||||||
|
return (char*) mret - (char*) _buf;
|
||||||
|
}
|
||||||
|
int32_t find (const char* str, int32_t pos = 0) const noexcept {return find (str, pos, strlen (str));}
|
||||||
|
|
||||||
|
/** Index of `ch` inside the string or -1 if not found. */
|
||||||
|
int32_t indexOf (char ch) const noexcept {
|
||||||
|
void* ret = memchr (_buf, ch, size());
|
||||||
|
return ret == nullptr ? -1 : (char*) ret - (char*) _buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helps to workaround the "statement has no effect" warning in `GSTRING_ON_STACK`.
|
||||||
|
gstring& self() noexcept {return *this;}
|
||||||
|
|
||||||
|
/** Grow buffer to be at least `to` characters long. */
|
||||||
|
void reserve (uint32_t to) {
|
||||||
|
uint32_t power = (_meta & CAPACITY_MASK) >> CAPACITY_OFFSET;
|
||||||
|
if (((uint32_t) 1 << power) < to) {
|
||||||
|
++power;
|
||||||
|
while (((uint32_t) 1 << power) < to) ++power;
|
||||||
|
if (power > 24) {GSTRING_ON_STACK (error, 64) << "gstring too large: " << (int) to; GTHROW (error.str());}
|
||||||
|
} else if (power) {
|
||||||
|
// No need to grow.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_meta = (_meta & ~CAPACITY_MASK) | (power << CAPACITY_OFFSET);
|
||||||
|
if (needsFreeing() && _buf != nullptr) {
|
||||||
|
_buf = ::realloc (_buf, capacity());
|
||||||
|
if (_buf == nullptr) GTHROW ("realloc failed");
|
||||||
|
} else {
|
||||||
|
const char* oldBuf = (const char*) _buf;
|
||||||
|
_buf = ::malloc (capacity());
|
||||||
|
if (_buf == nullptr) GTHROW ("malloc failed");
|
||||||
|
if (oldBuf != nullptr) ::memcpy (_buf, oldBuf, length());
|
||||||
|
_meta |= FREE_FLAG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Length setter. Useful when you manually write into the buffer or to cut the string. */
|
||||||
|
void length (uint32_t len) noexcept {
|
||||||
|
_meta = (_meta & ~LENGTH_MASK) | (len & LENGTH_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
friend class gstring_stream;
|
||||||
|
public:
|
||||||
|
/** Appends an integer to the string.
|
||||||
|
* @param base Radix, from 1 to 36 (default 10).
|
||||||
|
* @param bytes How many bytes to reserve (24 by default). */
|
||||||
|
void append64 (int64_t iv, int base = 10, uint_fast8_t bytes = 24) {
|
||||||
|
uint32_t pos = length();
|
||||||
|
if (capacity() < pos + bytes) reserve (pos + bytes);
|
||||||
|
length (itoa ((char*) _buf + pos, iv, base) - (char*) _buf);
|
||||||
|
}
|
||||||
|
void append (char ch) {
|
||||||
|
uint32_t pos = length();
|
||||||
|
const uint32_t cap = capacity();
|
||||||
|
if (pos >= cap || cap <= 1) reserve (pos + 1);
|
||||||
|
((char*)_buf)[pos] = ch;
|
||||||
|
length (++pos);
|
||||||
|
}
|
||||||
|
void append (const char* cstr, uint32_t clen) {
|
||||||
|
uint32_t len = length();
|
||||||
|
uint32_t need = len + clen;
|
||||||
|
const uint32_t cap = capacity();
|
||||||
|
if (need > cap || cap <= 1) reserve (need);
|
||||||
|
::memcpy ((char*) _buf + len, cstr, clen);
|
||||||
|
length (need);
|
||||||
|
}
|
||||||
|
/** This one is for http://code.google.com/p/re2/; `clear` then `append`. */
|
||||||
|
bool ParseFrom (const char* cstr, int clen) {
|
||||||
|
if (clen < 0 || clen > (int) LENGTH_MASK) return false;
|
||||||
|
length (0); append (cstr, (uint32_t) clen); return true;}
|
||||||
|
gstring& operator << (const gstring& gs) {append (gs.data(), gs.length()); return *this;}
|
||||||
|
gstring& operator << (const std::string& str) {append (str.data(), str.length()); return *this;}
|
||||||
|
gstring& operator << (const char* cstr) {if (cstr) append (cstr, ::strlen (cstr)); return *this;}
|
||||||
|
gstring& operator << (char ch) {append (ch); return *this;}
|
||||||
|
gstring& operator << (int iv) {append64 (iv, 10, sizeof (int) * 3); return *this;}
|
||||||
|
gstring& operator << (long iv) {append64 (iv, 10, sizeof (long) * 3); return *this;}
|
||||||
|
gstring& operator << (long long iv) {append64 (iv, 10, sizeof (long long) * 3); return *this;}
|
||||||
|
gstring& operator << (double dv) {
|
||||||
|
uint32_t len = length();
|
||||||
|
reserve (len + 32);
|
||||||
|
int rc = snprintf (endp(), 31, "%f", dv);
|
||||||
|
if (rc > 0) {length (len + std::min (rc, 31));}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator < (const gstring &gs) const noexcept {
|
||||||
|
uint32_t len1 = length(); uint32_t len2 = gs.length();
|
||||||
|
if (len1 == len2) return ::strncmp (data(), gs.data(), len1) < 0;
|
||||||
|
int cmp = ::strncmp (data(), gs.data(), std::min (len1, len2));
|
||||||
|
if (cmp) return cmp < 0;
|
||||||
|
return len1 < len2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asks `strftime` to generate a time string. Capacity is increased if necessary (up to a limit of +1024 bytes).
|
||||||
|
gstring& appendTime (const char* format, struct tm* tmv) {
|
||||||
|
int32_t pos = length(), cap = capacity(), left = cap - pos;
|
||||||
|
if (left < 8) {reserve (pos + 8); return appendTime (format, tmv);}
|
||||||
|
size_t got = strftime ((char*) _buf + pos, left, format, tmv);
|
||||||
|
if (got == 0) {
|
||||||
|
if (left > 1024) return *this; // Guard against perpetual growth.
|
||||||
|
reserve (pos + left * 2); return appendTime (format, tmv);
|
||||||
|
}
|
||||||
|
length (pos + got);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append the characters to this `gstring` wrapping them in the netstring format.
|
||||||
|
gstring& appendNetstring (const char* cstr, uint32_t clen) {
|
||||||
|
*this << (int) clen; append (':'); append (cstr, clen); append (','); return *this;}
|
||||||
|
/// Append the `gstr` wrapping it in the netstring format.
|
||||||
|
gstring& appendNetstring (const gstring& gstr) {return appendNetstring (gstr.data(), gstr.length());}
|
||||||
|
|
||||||
|
std::ostream& writeAsNetstring (std::ostream& stream) const;
|
||||||
|
|
||||||
|
/// Parse netstring at `pos` and return a `gstring` *pointing* at the parsed netstring.\n
|
||||||
|
/// No heap space allocated.\n
|
||||||
|
/// Throws std::runtime_error if netstring parsing fails.\n
|
||||||
|
/// If parsing was successfull, then `after` is set to point after the parsed netstring.
|
||||||
|
gstring netstringAt (uint32_t pos, uint32_t* after = nullptr) const {
|
||||||
|
const uint32_t len = length(); char* buf = (char*) _buf;
|
||||||
|
if (buf == nullptr) GTHROW ("gstring: netstringAt: nullptr");
|
||||||
|
uint32_t next = pos;
|
||||||
|
while (next < len && buf[next] >= '0' && buf[next] <= '9') ++next;
|
||||||
|
if (next >= len || buf[next] != ':' || next - pos > 10) GTHROW ("gstring: netstringAt: no header");
|
||||||
|
char* endptr = 0;
|
||||||
|
long nlen = ::strtol (buf + pos, &endptr, 10);
|
||||||
|
if (endptr != buf + next) GTHROW ("gstring: netstringAt: unexpected header end");
|
||||||
|
pos = next + 1; next = pos + nlen;
|
||||||
|
if (next >= len || buf[next] != ',') GTHROW ("gstring: netstringAt: no body");
|
||||||
|
if (after) *after = next + 1;
|
||||||
|
return gstring (0, buf + pos, false, next - pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around strtol, not entirely safe (make sure the string is terminated with a non-digit, by calling c_str, for example).
|
||||||
|
long intAt (uint32_t pos, uint32_t* after = nullptr, int base = 10) const {
|
||||||
|
// BTW: http://www.kumobius.com/2013/08/c-string-to-int/
|
||||||
|
const uint32_t len = length(); char* buf = (char*) _buf;
|
||||||
|
if (pos >= len || buf == nullptr) GTHROW ("gstring: intAt: pos >= len");
|
||||||
|
char* endptr = 0;
|
||||||
|
long lv = ::strtol (buf + pos, &endptr, base);
|
||||||
|
uint32_t next = endptr - buf;
|
||||||
|
if (next > len) GTHROW ("gstring: intAt: endptr > len");
|
||||||
|
if (after) *after = next;
|
||||||
|
return lv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around strtol. Copies the string into a temporary buffer in order to pass it to strtol. Empty string returns 0.
|
||||||
|
long toInt (int base = 10) const noexcept {
|
||||||
|
const uint32_t len = length(); if (len == 0) return 0;
|
||||||
|
char buf[len + 1]; memcpy (buf, _buf, len); buf[len] = 0;
|
||||||
|
return ::strtol (buf, nullptr, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single netstring from the `stream` and append it to the end of `gstring`.
|
||||||
|
/// Throws an exception if the input is not a well-formed netstring.
|
||||||
|
gstring& readNetstring (std::istream& stream) {
|
||||||
|
int32_t nlen; stream >> nlen;
|
||||||
|
if (!stream.good() || nlen < 0) GTHROW ("!netstring");
|
||||||
|
int ch = stream.get();
|
||||||
|
if (!stream.good() || ch != ':') GTHROW ("!netstring");
|
||||||
|
uint32_t glen = length();
|
||||||
|
const uint32_t cap = capacity();
|
||||||
|
if (cap < glen + nlen || cap <= 1) reserve (glen + nlen);
|
||||||
|
stream.read ((char*) _buf + glen, nlen);
|
||||||
|
if (!stream.good()) GTHROW ("!netstring");
|
||||||
|
ch = stream.get();
|
||||||
|
if (ch != ',') GTHROW ("!netstring");
|
||||||
|
length (glen + nlen);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set length to 0. `_buf` not changed.
|
||||||
|
gstring& clear() noexcept {length (0); return *this;}
|
||||||
|
|
||||||
|
/// Removes `count` characters starting at `pos`.
|
||||||
|
gstring& erase (uint32_t pos, uint32_t count = 1) noexcept {
|
||||||
|
const char* buf = (const char*) _buf;
|
||||||
|
const char* pt1 = buf + pos;
|
||||||
|
const char* pt2 = pt1 + count;
|
||||||
|
uint32_t len = length();
|
||||||
|
const char* end = buf + len;
|
||||||
|
if (pt2 <= end) {
|
||||||
|
length (len - count);
|
||||||
|
::memmove ((void*) pt1, (void*) pt2, end - pt2);
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
/// Remove characters [from,till) and return `from`.\n
|
||||||
|
/// Compatible with "boost/algorithm/string/trim.hpp".
|
||||||
|
iterator_t<char> erase (iterator_t<char> from, iterator_t<char> till) noexcept {
|
||||||
|
intptr_t ipos = from._ptr - (char*) _buf;
|
||||||
|
intptr_t count = till._ptr - from._ptr;
|
||||||
|
if (ipos >= 0 && count > 0) erase (ipos, count);
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
|
~gstring() noexcept {
|
||||||
|
if (_buf != nullptr && needsFreeing()) {::free (_buf); _buf = nullptr;}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool operator == (const gstring& gs1, const gstring& gs2) noexcept {return gs1.equals (gs2);}
|
||||||
|
inline bool operator == (const char* cstr, const gstring& gstr) noexcept {return gstr.equals (cstr);}
|
||||||
|
inline bool operator == (const gstring& gstr, const char* cstr) noexcept {return gstr.equals (cstr);}
|
||||||
|
inline bool operator != (const gstring& gs1, const gstring& gs2) noexcept {return !gs1.equals (gs2);}
|
||||||
|
inline bool operator != (const char* cstr, const gstring& gstr) noexcept {return !gstr.equals (cstr);}
|
||||||
|
inline bool operator != (const gstring& gstr, const char* cstr) noexcept {return !gstr.equals (cstr);}
|
||||||
|
|
||||||
|
inline bool operator == (const gstring& gstr, const std::string& str) noexcept {
|
||||||
|
return gstr.equals (gstring (gstring::ReferenceConstructor(), str.data(), str.size()));}
|
||||||
|
inline bool operator != (const gstring& gstr, const std::string& str) noexcept {return !(gstr == str);}
|
||||||
|
inline bool operator == (const std::string& str, const gstring& gstr) noexcept {return gstr == str;}
|
||||||
|
inline bool operator != (const std::string& str, const gstring& gstr) noexcept {return !(gstr == str);}
|
||||||
|
inline std::string operator += (std::string& str, const gstring& gstr) {return str.append (gstr.data(), gstr.size());}
|
||||||
|
inline std::string operator + (const std::string& str, const gstring& gstr) {return std::string (str) .append (gstr.data(), gstr.size());}
|
||||||
|
|
||||||
|
inline std::ostream& operator << (std::ostream& os, const gstring& gstr) {
|
||||||
|
if (gstr._buf != nullptr) os.write ((const char*) gstr._buf, gstr.length());
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode this `gstring` into `stream` as a netstring.
|
||||||
|
inline std::ostream& gstring::writeAsNetstring (std::ostream& stream) const {
|
||||||
|
stream << length() << ':' << *this << ',';
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://www.mr-edd.co.uk/blog/beginners_guide_streambuf
|
||||||
|
// http://www.dreamincode.net/code/snippet2499.htm
|
||||||
|
// http://spec.winprog.org/streams/
|
||||||
|
class gstring_stream: public std::basic_streambuf<char, std::char_traits<char> > {
|
||||||
|
gstring& _gstr;
|
||||||
|
public:
|
||||||
|
gstring_stream (gstring& gstr) noexcept: _gstr (gstr) {
|
||||||
|
char* buf = (char*) gstr._buf;
|
||||||
|
if (buf != nullptr) setg (buf, buf, buf + gstr.length());
|
||||||
|
}
|
||||||
|
protected:
|
||||||
|
virtual int_type overflow (int_type ch) {
|
||||||
|
if (__builtin_expect (ch != traits_type::eof(), 1)) _gstr.append ((char) ch);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// no copying
|
||||||
|
gstring_stream (const gstring_stream &);
|
||||||
|
gstring_stream& operator = (const gstring_stream &);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace glim
|
||||||
|
|
||||||
|
// hash specialization
|
||||||
|
// cf. http://stackoverflow.com/questions/8157937/how-to-specialize-stdhashkeyoperator-for-user-defined-type-in-unordered
|
||||||
|
namespace std {
|
||||||
|
template <> struct hash<glim::gstring> {
|
||||||
|
size_t operator()(const glim::gstring& gs) const noexcept {
|
||||||
|
// cf. http://stackoverflow.com/questions/7666509/hash-function-for-string
|
||||||
|
// Would be nice to use https://131002.net/siphash/ here.
|
||||||
|
uint32_t hash = 5381;
|
||||||
|
uint32_t len = gs.length();
|
||||||
|
if (len) {
|
||||||
|
const char* str = (const char*) gs._buf;
|
||||||
|
const char* end = str + len;
|
||||||
|
while (str < end) hash = ((hash << 5) + hash) + *str++; /* hash * 33 + c */
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _GSTRING_INCLUDED
|
|
@ -0,0 +1,255 @@
|
||||||
|
// Simple header-only wrapper around libevent's evhttp client.
|
||||||
|
// See also: https://github.com/cpp-netlib/cpp-netlib/issues/160
|
||||||
|
|
||||||
|
#ifndef _GLIM_HGET_INCLUDED
|
||||||
|
#define _GLIM_HGET_INCLUDED
|
||||||
|
|
||||||
|
#include <event2/event.h>
|
||||||
|
#include <event2/dns.h>
|
||||||
|
#include <evhttp.h> // http://stackoverflow.com/a/5237994; http://archives.seul.org/libevent/users/Sep-2010/msg00050.html
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <functional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include "exception.hpp"
|
||||||
|
#include "gstring.hpp"
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
/// HTTP results
|
||||||
|
struct hgot {
|
||||||
|
int32_t status = 0;
|
||||||
|
/// Uses errno codes.
|
||||||
|
int32_t error = 0;
|
||||||
|
struct evbuffer* body = 0;
|
||||||
|
struct evhttp_request* req = 0;
|
||||||
|
size_t bodyLength() const {return body ? evbuffer_get_length (body) : 0;}
|
||||||
|
/// Warning: the string is NOT zero-terminated.
|
||||||
|
const char* bodyData() {return body ? (const char*) evbuffer_pullup (body, -1) : "";}
|
||||||
|
/// Returns a zero-terminated string. Warning: modifies the `body` every time in order to add the terminator.
|
||||||
|
const char* cbody() {if (!body) return ""; evbuffer_add (body, "", 1); return (const char*) evbuffer_pullup (body, -1);}
|
||||||
|
/// A gstring *view* into the `body`.
|
||||||
|
glim::gstring gbody() {
|
||||||
|
if (!body) return glim::gstring();
|
||||||
|
return glim::gstring (glim::gstring::ReferenceConstructor(), (const char*) evbuffer_pullup (body, -1), evbuffer_get_length (body));}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Used internally to pass both connection and handler into callback.
|
||||||
|
struct hgetContext {
|
||||||
|
struct evhttp_connection* conn;
|
||||||
|
std::function<void(hgot&)> handler;
|
||||||
|
hgetContext (struct evhttp_connection* conn, std::function<void(hgot&)> handler): conn (conn), handler (handler) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Invoked when evhttp finishes a request.
|
||||||
|
inline void hgetCB (struct evhttp_request* req, void* ctx_){
|
||||||
|
hgetContext* ctx = (hgetContext*) ctx_;
|
||||||
|
|
||||||
|
hgot gt;
|
||||||
|
if (req == NULL) gt.error = ETIMEDOUT;
|
||||||
|
else if (req->response_code == 0) gt.error = ECONNREFUSED;
|
||||||
|
else {
|
||||||
|
gt.status = req->response_code;
|
||||||
|
gt.body = req->input_buffer;
|
||||||
|
gt.req = req;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx->handler (gt);
|
||||||
|
} catch (const std::runtime_error& ex) { // Shouldn't normally happen:
|
||||||
|
std::cerr << "glim::hget, handler exception: " << ex.what() << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
evhttp_connection_free ((struct evhttp_connection*) ctx->conn);
|
||||||
|
//freed by libevent//if (req != NULL) evhttp_request_free (req);
|
||||||
|
delete ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
C++ wrapper around libevent's http client.
|
||||||
|
Example: \code
|
||||||
|
hget (evbase, dnsbase) .setRequestBuilder ([](struct evhttp_request* req){
|
||||||
|
evbuffer_add (req->output_buffer, "foo", 3);
|
||||||
|
evhttp_add_header (req->output_headers, "Content-Length", "3");
|
||||||
|
}) .go ("http://127.0.0.1:8080/test", [](hgot& got){
|
||||||
|
if (got.error) log_warn ("127.0.0.1:8080 " << strerror (got.error));
|
||||||
|
else if (got.status != 200) log_warn ("127.0.0.1:8080 != 200");
|
||||||
|
else log_info ("got " << evbuffer_get_length (got.body) << " bytes from /test: " << evbuffer_pullup (got.body, -1));
|
||||||
|
}); \endcode
|
||||||
|
*/
|
||||||
|
class hget {
|
||||||
|
public:
|
||||||
|
std::shared_ptr<struct event_base> _evbase;
|
||||||
|
std::shared_ptr<struct evdns_base> _dnsbase;
|
||||||
|
std::function<void(struct evhttp_request*)> _requestBuilder;
|
||||||
|
enum evhttp_cmd_type _method;
|
||||||
|
public:
|
||||||
|
typedef std::shared_ptr<struct evhttp_uri> uri_t;
|
||||||
|
/// The third parameter is the request number, starting from 1.
|
||||||
|
typedef std::function<float(hgot&,uri_t,int32_t)> until_handler_t;
|
||||||
|
public:
|
||||||
|
hget (std::shared_ptr<struct event_base> evbase, std::shared_ptr<struct evdns_base> dnsbase):
|
||||||
|
_evbase (evbase), _dnsbase (dnsbase), _method (EVHTTP_REQ_GET) {}
|
||||||
|
|
||||||
|
/// Modifies the request before its execution.
|
||||||
|
hget& setRequestBuilder (std::function<void(struct evhttp_request*)> rb) {
|
||||||
|
_requestBuilder = rb;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Uses a simple request builder to send the `str`.
|
||||||
|
* `str` is a `char` string class with methods `data` and `size`. */
|
||||||
|
template<typename STR> hget& payload (STR str, const char* contentType = nullptr, enum evhttp_cmd_type method = EVHTTP_REQ_POST) {
|
||||||
|
_method = method;
|
||||||
|
return setRequestBuilder ([str,contentType](struct evhttp_request* req) {
|
||||||
|
if (contentType) evhttp_add_header (req->output_headers, "Content-Type", contentType);
|
||||||
|
char buf[64];
|
||||||
|
*glim::itoa (buf, (int) str.size()) = 0;
|
||||||
|
evhttp_add_header (req->output_headers, "Content-Length", buf);
|
||||||
|
evbuffer_add (req->output_buffer, (const void*) str.data(), (size_t) str.size());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct evhttp_request* go (uri_t uri, int32_t timeoutSec, std::function<void(hgot&)> handler) {
|
||||||
|
int port = evhttp_uri_get_port (uri.get());
|
||||||
|
if (port == -1) port = 80;
|
||||||
|
struct evhttp_connection* conn = evhttp_connection_base_new (_evbase.get(), _dnsbase.get(),
|
||||||
|
evhttp_uri_get_host (uri.get()), port);
|
||||||
|
evhttp_connection_set_timeout (conn, timeoutSec);
|
||||||
|
struct evhttp_request *req = evhttp_request_new (hgetCB, new hgetContext(conn, handler));
|
||||||
|
int ret = evhttp_add_header (req->output_headers, "Host", evhttp_uri_get_host (uri.get()));
|
||||||
|
if (ret) throw std::runtime_error ("hget: evhttp_add_header(Host) != 0");
|
||||||
|
if (_requestBuilder) _requestBuilder (req);
|
||||||
|
const char* get = evhttp_uri_get_path (uri.get());
|
||||||
|
const char* qs = evhttp_uri_get_query (uri.get());
|
||||||
|
if (qs == NULL) {
|
||||||
|
ret = evhttp_make_request (conn, req, _method, get);
|
||||||
|
} else {
|
||||||
|
size_t getLen = strlen (get);
|
||||||
|
size_t qsLen = strlen (qs);
|
||||||
|
char buf[getLen + 1 + qsLen + 1];
|
||||||
|
char* caret = stpcpy (buf, get);
|
||||||
|
*caret++ = '?';
|
||||||
|
caret = stpcpy (caret, qs);
|
||||||
|
assert (caret - buf < sizeof (buf));
|
||||||
|
ret = evhttp_make_request (conn, req, _method, buf);
|
||||||
|
}
|
||||||
|
if (ret) throw std::runtime_error ("hget: evhttp_make_request != 0");
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
struct evhttp_request* go (const char* url, int32_t timeoutSec, std::function<void(hgot&)> handler) {
|
||||||
|
return go (std::shared_ptr<struct evhttp_uri> (evhttp_uri_parse (url), evhttp_uri_free), timeoutSec, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
void goUntil (std::vector<uri_t> urls, until_handler_t handler, int32_t timeoutSec = 20);
|
||||||
|
/**
|
||||||
|
Parse urls and call `goUntil`.
|
||||||
|
Example (trying ten times to reach the servers): \code
|
||||||
|
std::string path ("/path");
|
||||||
|
hget.goUntilS (boost::assign::list_of ("http://server1" + path) ("http://server2" + path),
|
||||||
|
[](hgot& got, hget::uri_t uri, int32_t num)->float {
|
||||||
|
std::cout << "server: " << evhttp_uri_get_host (uri.get()) << "; request number: " << num << std::endl;
|
||||||
|
if (got.status != 200 && num < 10) return 1.f; // Retry in a second.
|
||||||
|
return -1.f; // No need to retry the request.
|
||||||
|
});
|
||||||
|
\endcode
|
||||||
|
@param urls is a for-compatible container of strings (where string has methods `data` and `size`).
|
||||||
|
*/
|
||||||
|
template<typename URLS> void goUntilS (URLS&& urls, until_handler_t handler, int32_t timeoutSec = 20) {
|
||||||
|
std::vector<uri_t> parsedUrls;
|
||||||
|
for (auto&& url: urls) {
|
||||||
|
// Copying to stack might be cheaper than malloc in c_str.
|
||||||
|
int len = url.size(); char buf[len + 1]; memcpy (buf, url.data(), len); buf[len] = 0;
|
||||||
|
struct evhttp_uri* uri = evhttp_uri_parse (buf);
|
||||||
|
if (!uri) GTHROW (std::string ("!evhttp_uri_parse: ") + buf);
|
||||||
|
parsedUrls.push_back (uri_t (uri, evhttp_uri_free));
|
||||||
|
}
|
||||||
|
goUntil (parsedUrls, handler, timeoutSec);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
Parse urls and call `goUntil`.
|
||||||
|
Example (trying ten times to reach the servers): \code
|
||||||
|
hget.goUntilC (boost::assign::list_of ("http://server1/") ("http://server2/"),
|
||||||
|
[](hgot& got, hget::uri_t uri, int32_t num)->float {
|
||||||
|
std::cout << "server: " << evhttp_uri_get_host (uri.get()) << "; request number: " << num << std::endl;
|
||||||
|
if (got.status != 200 && num < 10) return 1.f; // Retry in a second.
|
||||||
|
return -1.f; // No need to retry the request.
|
||||||
|
});
|
||||||
|
\endcode
|
||||||
|
Or with `std::array` instead of `boost::assign::list_of`: \code
|
||||||
|
std::array<const char*, 2> urls {{"http://server1/", "http://server2/"}};
|
||||||
|
hget.goUntilC (urls, [](hgot& got, hget::uri_t uri, int32_t num)->float {
|
||||||
|
return got.status != 200 && num < 10 ? 0.f : -1.f;});
|
||||||
|
\endcode
|
||||||
|
@param urls is a for-compatible container of C strings (const char*).
|
||||||
|
*/
|
||||||
|
template<typename URLS> void goUntilC (URLS&& urls, until_handler_t handler, int32_t timeoutSec = 20) {
|
||||||
|
std::vector<uri_t> parsedUrls;
|
||||||
|
for (auto url: urls) {
|
||||||
|
struct evhttp_uri* uri = evhttp_uri_parse (url);
|
||||||
|
if (!uri) GTHROW (std::string ("Can't parse url: ") + url);
|
||||||
|
parsedUrls.push_back (uri_t (uri, evhttp_uri_free));
|
||||||
|
}
|
||||||
|
goUntil (parsedUrls, handler, timeoutSec);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void hgetUntilRetryCB (evutil_socket_t, short, void* utilHandlerPtr); // event_callback_fn
|
||||||
|
|
||||||
|
/** `hget::goUntil` implementation.
|
||||||
|
* This function object is passed to `hget::go` as a handler and calls `hget::go` again if necessary. */
|
||||||
|
struct HgetUntilHandler {
|
||||||
|
hget _hget;
|
||||||
|
hget::until_handler_t _handler;
|
||||||
|
std::vector<hget::uri_t> _urls;
|
||||||
|
int32_t _timeoutSec;
|
||||||
|
int32_t _requestNum;
|
||||||
|
uint8_t _nextUrl; ///< A round-robin pointer to the next url in `_urls`.
|
||||||
|
HgetUntilHandler (hget& hg, hget::until_handler_t handler, std::vector<hget::uri_t> urls, int32_t timeoutSec):
|
||||||
|
_hget (hg), _handler (handler), _urls (urls), _timeoutSec (timeoutSec), _requestNum (0), _nextUrl (0) {}
|
||||||
|
void operator() (hgot& got) {
|
||||||
|
uint8_t urlNum = _nextUrl ? _nextUrl - 1 : _urls.size() - 1;
|
||||||
|
float retryAfterSec = _handler (got, _urls[urlNum], _requestNum);
|
||||||
|
if (retryAfterSec == 0.f) retry();
|
||||||
|
else if (retryAfterSec > 0.f) {
|
||||||
|
struct timeval wait;
|
||||||
|
wait.tv_sec = (int) retryAfterSec;
|
||||||
|
retryAfterSec -= wait.tv_sec;
|
||||||
|
wait.tv_usec = (int) (retryAfterSec * 1000000.f);
|
||||||
|
int rc = event_base_once (_hget._evbase.get(), -1, EV_TIMEOUT, hgetUntilRetryCB, new HgetUntilHandler (*this), &wait);
|
||||||
|
if (rc) throw std::runtime_error ("HgetUntilHandler: event_base_once != 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void start() {retry();}
|
||||||
|
void retry() {
|
||||||
|
uint8_t nextUrl = _nextUrl++;
|
||||||
|
if (_nextUrl >= _urls.size()) _nextUrl = 0;
|
||||||
|
++_requestNum;
|
||||||
|
_hget.go (_urls[nextUrl], _timeoutSec, *this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Used in `hget::goUntil` to wait in `evtimer_new` before repeating the request.
|
||||||
|
inline void hgetUntilRetryCB (evutil_socket_t, short, void* utilHandlerPtr) { // event_callback_fn
|
||||||
|
std::unique_ptr<HgetUntilHandler> untilHandler ((HgetUntilHandler*) utilHandlerPtr);
|
||||||
|
untilHandler->retry();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to retry the request using multiple URLs in a round-robin fashion.
|
||||||
|
* The `handler` returns the number of seconds to wait before retrying the request or -1 if no retry is necessary.
|
||||||
|
*/
|
||||||
|
inline void hget::goUntil (std::vector<uri_t> urls, until_handler_t handler, int32_t timeoutSec) {
|
||||||
|
HgetUntilHandler (*this, handler, urls, timeoutSec) .start();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _GLIM_HGET_INCLUDED
|
|
@ -0,0 +1,384 @@
|
||||||
|
#ifndef _GLIM_LDB_HPP_INCLUDED
|
||||||
|
#define _GLIM_LDB_HPP_INCLUDED
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leveldb (http://code.google.com/p/leveldb/) wrapper.
|
||||||
|
* @code
|
||||||
|
Copyright 2012 Kozarezov Artem Aleksandrovich
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
* @endcode
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
//#include <unordered_map> // having SIGFPE as in http://stackoverflow.com/q/13580823/257568
|
||||||
|
#include <map>
|
||||||
|
#include <climits> // CHAR_MAX
|
||||||
|
|
||||||
|
#include <leveldb/db.h>
|
||||||
|
#include <leveldb/write_batch.h>
|
||||||
|
#include <leveldb/filter_policy.h>
|
||||||
|
#include <boost/archive/binary_oarchive.hpp>
|
||||||
|
#include <boost/archive/binary_iarchive.hpp>
|
||||||
|
#include <boost/serialization/serialization.hpp>
|
||||||
|
#include <boost/noncopyable.hpp>
|
||||||
|
#include <boost/iterator/iterator_facade.hpp>
|
||||||
|
#include <boost/range/iterator_range.hpp> // http://www.boost.org/doc/libs/1_52_0/libs/range/doc/html/range/reference/utilities/iterator_range.html
|
||||||
|
|
||||||
|
#include <arpa/inet.h> // htonl, ntohl
|
||||||
|
#include <sys/stat.h> // mkdir
|
||||||
|
#include <sys/types.h> // mkdir
|
||||||
|
#include <string.h> // strerror
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "gstring.hpp"
|
||||||
|
#include "exception.hpp"
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
G_DEFINE_EXCEPTION (LdbEx);
|
||||||
|
|
||||||
|
template <typename T> inline void ldbSerialize (gstring& bytes, const T& data) {
|
||||||
|
gstring_stream stream (bytes);
|
||||||
|
boost::archive::binary_oarchive oa (stream, boost::archive::no_header);
|
||||||
|
oa << data;
|
||||||
|
}
|
||||||
|
template <typename V> inline void ldbDeserialize (const gstring& bytes, V& data) {
|
||||||
|
gstring_stream stream (const_cast<gstring&> (bytes));
|
||||||
|
boost::archive::binary_iarchive ia (stream, boost::archive::no_header);
|
||||||
|
ia >> data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** uint32_t keys are stored big-endian (network byte order) in order to be compatible with lexicographic ordering. */
|
||||||
|
template <> inline void ldbSerialize<uint32_t> (gstring& bytes, const uint32_t& ui) {
|
||||||
|
uint32_t nui = htonl (ui); bytes.append ((const char*) &nui, sizeof (uint32_t));}
|
||||||
|
/** Deserialize uint32_t from big-endian (network byte order). */
|
||||||
|
template <> inline void ldbDeserialize<uint32_t> (const gstring& bytes, uint32_t& ui) {
|
||||||
|
if (bytes.size() != sizeof (uint32_t)) GNTHROW (LdbEx, "Not uint32_t, wrong number of bytes");
|
||||||
|
uint32_t nui = * (uint32_t*) bytes.data(); ui = ntohl (nui);}
|
||||||
|
|
||||||
|
/** If the data is `gstring` then use the data's buffer directly, no copy. */
|
||||||
|
template <> inline void ldbSerialize<gstring> (gstring& bytes, const gstring& data) {
|
||||||
|
bytes = gstring (0, (void*) data.data(), false, data.length());}
|
||||||
|
/** Deserializing into `gstring` copies the bytes into it, reusing its buffer. */
|
||||||
|
template <> inline void ldbDeserialize<gstring> (const gstring& bytes, gstring& data) {
|
||||||
|
data.clear() << bytes;}
|
||||||
|
|
||||||
|
/** If the data is `std::string` then use the data's buffer directly, no copy. */
|
||||||
|
template <> inline void ldbSerialize<std::string> (gstring& bytes, const std::string& data) {
|
||||||
|
bytes = gstring (0, (void*) data.data(), false, data.length());}
|
||||||
|
/** Deserializing into `std::string` copies the bytes into it, reusing its buffer. */
|
||||||
|
template <> inline void ldbDeserialize<std::string> (const gstring& bytes, std::string& data) {
|
||||||
|
data.clear(); data.append (bytes.data(), bytes.size());}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header-only Leveldb wrapper.\n
|
||||||
|
* Uses Boost Serialization to pack keys and values (glim::gstring can be used for raw bytes).\n
|
||||||
|
* Allows semi-automatic indexing with triggers.
|
||||||
|
*/
|
||||||
|
struct Ldb {
|
||||||
|
std::shared_ptr<leveldb::DB> _db;
|
||||||
|
std::shared_ptr<const leveldb::FilterPolicy> _filter;
|
||||||
|
|
||||||
|
struct IteratorEntry { ///< Something to be `dereference`d from the Iterator. Also a pImpl allowing to keep the `_valid` and the `_lit` in sync.
|
||||||
|
leveldb::Iterator* _lit;
|
||||||
|
bool _valid:1;
|
||||||
|
|
||||||
|
IteratorEntry (const IteratorEntry&) = delete; // Owns `leveldb::Iterator`, should not be copied.
|
||||||
|
IteratorEntry (IteratorEntry&&) = default;
|
||||||
|
|
||||||
|
IteratorEntry (leveldb::Iterator* lit, bool valid = false): _lit (lit), _valid (valid) {}
|
||||||
|
~IteratorEntry() {delete _lit;}
|
||||||
|
|
||||||
|
/** Zero-copy view of the current key bytes. Should *not* be used after the Iterator is changed or destroyed.
|
||||||
|
* @param ref If true then the *copies* of the returned gstring will keep pointing to LevelDB memory which is valid only until the iterator changes. */
|
||||||
|
const gstring keyView (bool ref = false) const {
|
||||||
|
if (!_valid) return gstring();
|
||||||
|
const leveldb::Slice& key = _lit->key();
|
||||||
|
return gstring (0, (void*) key.data(), false, key.size(), ref);} // Zero copy.
|
||||||
|
/** Zero-copy view of the current value bytes. Should *not* be used after the Iterator is changed or destroyed.
|
||||||
|
* @param ref If true then the *copies* of the returned gstring will keep pointing to LevelDB memory which is valid only until the iterator changes. */
|
||||||
|
const gstring valueView (bool ref = false) const {
|
||||||
|
if (!_valid) return gstring();
|
||||||
|
const leveldb::Slice& val = _lit->value();
|
||||||
|
return gstring (0, (void*) val.data(), false, val.size(), ref);} // Zero copy.
|
||||||
|
/** Deserialize into `key`. */
|
||||||
|
template <typename T> void getKey (T& key) const {ldbDeserialize (keyView (true), key);}
|
||||||
|
/** Deserialize the key into a temporary and return it. */
|
||||||
|
template <typename T> T getKey() const {T key; getKey (key); return key;}
|
||||||
|
/** Deserialize into `value`. */
|
||||||
|
template <typename T> void getValue (T& value) const {ldbDeserialize (valueView (true), value);}
|
||||||
|
/** Deserialize the value into a temporary and return it. */
|
||||||
|
template <typename T> T getValue() const {T value; getValue (value); return value;}
|
||||||
|
};
|
||||||
|
struct NoSeekFlag {}; ///< Tells the `Iterator` constructor not to seek to the beginning of the database.
|
||||||
|
/** Wraps Leveldb iterator.
|
||||||
|
* Note: "In fact the iterator is a light-weight snapshot. It will see exactly the version of the DB that existed when the iterator was created
|
||||||
|
* (i.e., any insert/delete done after the iterator is created, regardless of which thread does them) will be invisible to the iterator"
|
||||||
|
* (https://groups.google.com/d/msg/leveldb/nX8S5KKiSn4/PI92Yf1Hf6UJ). */
|
||||||
|
struct Iterator: public boost::iterator_facade<Iterator, IteratorEntry, boost::bidirectional_traversal_tag> {
|
||||||
|
std::shared_ptr<IteratorEntry> _entry; ///< The Iterator might be copied around, therefore we keep the real iterator and the state in the shared_ptr.
|
||||||
|
|
||||||
|
Iterator (const Iterator&) = default;
|
||||||
|
Iterator (Iterator&&) = default;
|
||||||
|
Iterator& operator= (const Iterator&) = default;
|
||||||
|
Iterator& operator= (Iterator&&) = default;
|
||||||
|
/** Iterate from the beginning or the end of the database.
|
||||||
|
* @param position can be MDB_FIRST or MDB_LAST */
|
||||||
|
Iterator (Ldb* ldb, leveldb::ReadOptions options = leveldb::ReadOptions()):
|
||||||
|
_entry (std::make_shared<IteratorEntry> (ldb->_db->NewIterator (options))) {
|
||||||
|
IteratorEntry* entry = _entry.get();
|
||||||
|
entry->_lit->SeekToFirst();
|
||||||
|
entry->_valid = entry->_lit->Valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterator (Ldb* ldb, NoSeekFlag, leveldb::ReadOptions options = leveldb::ReadOptions()):
|
||||||
|
_entry (std::make_shared<IteratorEntry> (ldb->_db->NewIterator (options))) {}
|
||||||
|
/** True if the iterator isn't pointing anywhere. */
|
||||||
|
bool end() const {return !_entry->_valid;}
|
||||||
|
|
||||||
|
bool equal (const Iterator& other) const {
|
||||||
|
bool weAreValid = _entry->_valid, theyAreValid = other._entry->_valid;
|
||||||
|
if (!weAreValid) return !theyAreValid;
|
||||||
|
if (!theyAreValid) return false;
|
||||||
|
auto&& ourKey = _entry->_lit->key(), theirKey = other._entry->_lit->key();
|
||||||
|
if (ourKey.size() != theirKey.size()) return false;
|
||||||
|
return memcmp (ourKey.data(), theirKey.data(), ourKey.size()) == 0;
|
||||||
|
}
|
||||||
|
IteratorEntry& dereference() const {
|
||||||
|
// NB: Boost iterator_facade expects the `dereference` to be a `const` method.
|
||||||
|
// I guess Iterator is not modified, so the `dereference` is `const`, even though the Entry can be modified.
|
||||||
|
return *_entry;
|
||||||
|
}
|
||||||
|
virtual void increment() {
|
||||||
|
IteratorEntry* entry = _entry.get();
|
||||||
|
if (entry->_valid) entry->_lit->Next();
|
||||||
|
else entry->_lit->SeekToFirst();
|
||||||
|
entry->_valid = entry->_lit->Valid();
|
||||||
|
}
|
||||||
|
virtual void decrement() {
|
||||||
|
IteratorEntry* entry = _entry.get();
|
||||||
|
if (entry->_valid) entry->_lit->Prev();
|
||||||
|
else entry->_lit->SeekToLast();
|
||||||
|
entry->_valid = entry->_lit->Valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterator& seek (const gstring& key) {
|
||||||
|
IteratorEntry* entry = _entry.get();
|
||||||
|
entry->_lit->Seek (leveldb::Slice (key.data(), key.size()));
|
||||||
|
entry->_valid = entry->_lit->Valid();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Iterator begin() {return Iterator (this);}
|
||||||
|
Iterator end() {return Iterator (this, NoSeekFlag());}
|
||||||
|
|
||||||
|
/** Range from `from` (inclusive) to `till` (exclusive). */
|
||||||
|
template <typename K>
|
||||||
|
boost::iterator_range<Iterator> range (const K& from, const K& till, leveldb::ReadOptions options = leveldb::ReadOptions()) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
ldbSerialize (kbytes, from);
|
||||||
|
Iterator fit (this, NoSeekFlag(), options); fit.seek (kbytes);
|
||||||
|
|
||||||
|
ldbSerialize (kbytes.clear(), till);
|
||||||
|
Iterator tit (this, NoSeekFlag(), options); tit.seek (kbytes);
|
||||||
|
|
||||||
|
return boost::iterator_range<Iterator> (fit, tit);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StartsWithIterator: public Iterator {
|
||||||
|
gstring _starts;
|
||||||
|
StartsWithIterator (const StartsWithIterator&) = default;
|
||||||
|
StartsWithIterator (StartsWithIterator&&) = default;
|
||||||
|
StartsWithIterator& operator= (const StartsWithIterator&) = default;
|
||||||
|
StartsWithIterator& operator= (StartsWithIterator&&) = default;
|
||||||
|
|
||||||
|
StartsWithIterator (Ldb* ldb, const char* data, uint32_t length, leveldb::ReadOptions options = leveldb::ReadOptions()):
|
||||||
|
Iterator (ldb, NoSeekFlag(), options), _starts (data, length) {
|
||||||
|
IteratorEntry* entry = _entry.get();
|
||||||
|
entry->_lit->Seek (leveldb::Slice (data, length));
|
||||||
|
entry->_valid = checkValidity();
|
||||||
|
}
|
||||||
|
/** End iterator, pointing nowhere. */
|
||||||
|
StartsWithIterator (Ldb* ldb, const char* data, uint32_t length, NoSeekFlag, leveldb::ReadOptions options = leveldb::ReadOptions()):
|
||||||
|
Iterator (ldb, NoSeekFlag(), options), _starts (data, length) {}
|
||||||
|
|
||||||
|
bool checkValidity() const {
|
||||||
|
IteratorEntry* entry = _entry.get();
|
||||||
|
return entry->_lit->Valid() && entry->_lit->key().starts_with (leveldb::Slice (_starts.data(), _starts.length()));
|
||||||
|
}
|
||||||
|
virtual void increment() override {
|
||||||
|
IteratorEntry* entry = _entry.get();
|
||||||
|
if (entry->_valid) entry->_lit->Next();
|
||||||
|
else entry->_lit->Seek (leveldb::Slice (_starts.data(), _starts.length()));
|
||||||
|
entry->_valid = checkValidity();
|
||||||
|
}
|
||||||
|
virtual void decrement() override {
|
||||||
|
IteratorEntry* entry = _entry.get();
|
||||||
|
if (entry->_valid) entry->_lit->Prev(); else seekLast();
|
||||||
|
entry->_valid = checkValidity();
|
||||||
|
}
|
||||||
|
void seekLast() {
|
||||||
|
leveldb::Iterator* lit = _entry->_lit;
|
||||||
|
// Go somewhere *below* the `_starts` prefix.
|
||||||
|
char after[_starts.length()]; if (sizeof (after)) {
|
||||||
|
memcpy (after, _starts.data(), sizeof (after));
|
||||||
|
uint32_t pos = sizeof (after); while (--pos >= 0) if (after[pos] < CHAR_MAX) {++after[pos]; break;}
|
||||||
|
if (pos >= 0) {lit->Seek (leveldb::Slice (after, sizeof (after))); if (!lit->Valid()) lit->SeekToLast();} else lit->SeekToLast();
|
||||||
|
} else lit->SeekToLast();
|
||||||
|
// Seek back until we are in the `_starts` prefix.
|
||||||
|
for (leveldb::Slice prefix (_starts.data(), _starts.length()); lit->Valid(); lit->Prev()) {
|
||||||
|
leveldb::Slice key (lit->key());
|
||||||
|
if (key.starts_with (prefix)) break; // We're "back" in the `_starts` prefix.
|
||||||
|
if (key.compare (prefix) < 0) break; // Gone too far (no prefix entries in the db).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Range over entries starting with `key`. */
|
||||||
|
template <typename K>
|
||||||
|
boost::iterator_range<StartsWithIterator> startsWith (const K& key) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
ldbSerialize (kbytes, key);
|
||||||
|
return boost::iterator_range<StartsWithIterator> (
|
||||||
|
StartsWithIterator (this, kbytes.data(), kbytes.length()),
|
||||||
|
StartsWithIterator (this, kbytes.data(), kbytes.length(), NoSeekFlag()));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Trigger {
|
||||||
|
virtual gstring triggerName() const {return C2GSTRING ("defaultTriggerName");};
|
||||||
|
virtual void put (Ldb& ldb, void* key, gstring& kbytes, void* value, gstring& vbytes, leveldb::WriteBatch& batch) = 0;
|
||||||
|
virtual void del (Ldb& ldb, void* key, gstring& kbytes, leveldb::WriteBatch& batch) = 0;
|
||||||
|
};
|
||||||
|
std::map<gstring, std::shared_ptr<Trigger>> _triggers;
|
||||||
|
|
||||||
|
/** Register the trigger (by its `triggerName`). */
|
||||||
|
void putTrigger (std::shared_ptr<Trigger> trigger) {
|
||||||
|
_triggers[trigger->triggerName()] = trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
Ldb() {}
|
||||||
|
|
||||||
|
/** Opens Leveldb database. */
|
||||||
|
Ldb (const char* path, leveldb::Options* options = nullptr, mode_t mode = 0770) {
|
||||||
|
int rc = ::mkdir (path, mode);
|
||||||
|
if (rc && errno != EEXIST) GNTHROW (LdbEx, std::string ("Can't create ") + path + ": " + ::strerror (errno));
|
||||||
|
leveldb::DB* db;
|
||||||
|
leveldb::Status status;
|
||||||
|
if (options) {
|
||||||
|
status = leveldb::DB::Open (*options, path, &db);
|
||||||
|
} else {
|
||||||
|
leveldb::Options localOptions;
|
||||||
|
localOptions.create_if_missing = true;
|
||||||
|
_filter.reset (leveldb::NewBloomFilterPolicy (8));
|
||||||
|
localOptions.filter_policy = _filter.get();
|
||||||
|
status = leveldb::DB::Open (localOptions, path, &db);
|
||||||
|
}
|
||||||
|
if (!status.ok()) GNTHROW (LdbEx, std::string ("Ldb: Can't open ") + path + ": " + status.ToString());
|
||||||
|
_db.reset (db);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wraps an existing Leveldb handler. */
|
||||||
|
Ldb (std::shared_ptr<leveldb::DB> db): _db (db) {}
|
||||||
|
|
||||||
|
template <typename K, typename V> void put (const K& key, const V& value, leveldb::WriteBatch& batch) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
ldbSerialize (kbytes, key);
|
||||||
|
|
||||||
|
char vbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring vbytes (sizeof (vbuf), vbuf, false, 0);
|
||||||
|
ldbSerialize (vbytes, value);
|
||||||
|
|
||||||
|
for (auto& trigger: _triggers) trigger.second->put (*this, (void*) &key, kbytes, (void*) &value, vbytes, batch);
|
||||||
|
|
||||||
|
batch.Put (leveldb::Slice (kbytes.data(), kbytes.size()), leveldb::Slice (vbytes.data(), vbytes.size()));
|
||||||
|
}
|
||||||
|
template <typename K, typename V> void put (const K& key, const V& value) {
|
||||||
|
leveldb::WriteBatch batch;
|
||||||
|
put (key, value, batch);
|
||||||
|
leveldb::Status status (_db->Write (leveldb::WriteOptions(), &batch));
|
||||||
|
if (!status.ok()) GNTHROW (LdbEx, "Ldb: add: " + status.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if the key exists. Throws on error. */
|
||||||
|
template <typename K> bool have (const K& key, leveldb::ReadOptions options = leveldb::ReadOptions()) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
ldbSerialize (kbytes, key);
|
||||||
|
leveldb::Slice keySlice (kbytes.data(), kbytes.size());
|
||||||
|
|
||||||
|
// NB: "BloomFilter only helps for Get() calls" - https://groups.google.com/d/msg/leveldb/oEiDztqHiHc/LMY3tHxzRGAJ
|
||||||
|
// "Apart from the lack of Bloom filter functionality, creating an iterator is really quite slow" - qpu2jSA8mCEJ
|
||||||
|
std::string str;
|
||||||
|
leveldb::Status status (_db->Get (options, keySlice, &str));
|
||||||
|
if (status.ok()) return true;
|
||||||
|
else if (status.IsNotFound()) return false;
|
||||||
|
else GTHROW ("Ldb.have: " + status.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` and modifies `value` if `key` is found. */
|
||||||
|
template <typename K, typename V> bool get (const K& key, V& value, leveldb::ReadOptions options = leveldb::ReadOptions()) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
ldbSerialize (kbytes, key);
|
||||||
|
leveldb::Slice keySlice (kbytes.data(), kbytes.size());
|
||||||
|
|
||||||
|
// NB: "BloomFilter only helps for Get() calls" - https://groups.google.com/d/msg/leveldb/oEiDztqHiHc/LMY3tHxzRGAJ
|
||||||
|
// "Apart from the lack of Bloom filter functionality, creating an iterator is really quite slow" - qpu2jSA8mCEJ
|
||||||
|
std::string str;
|
||||||
|
leveldb::Status status (_db->Get (options, keySlice, &str));
|
||||||
|
if (status.ok()) {
|
||||||
|
ldbDeserialize (gstring (0, (void*) str.data(), false, str.size()), value);
|
||||||
|
return true;
|
||||||
|
} else if (status.IsNotFound()) return false;
|
||||||
|
else GTHROW ("Ldb.get: " + status.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename K> void del (const K& key, leveldb::WriteBatch& batch) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
ldbSerialize (kbytes, key);
|
||||||
|
if (kbytes.empty()) GNTHROW (LdbEx, "del: key is empty");
|
||||||
|
|
||||||
|
for (auto& trigger: _triggers) trigger.second->del (*this, (void*) &key, kbytes, batch);
|
||||||
|
|
||||||
|
batch.Delete (leveldb::Slice (kbytes.data(), kbytes.size()));
|
||||||
|
}
|
||||||
|
template <typename K> void del (const K& key) {
|
||||||
|
leveldb::WriteBatch batch;
|
||||||
|
del (key, batch);
|
||||||
|
leveldb::Status status (_db->Write (leveldb::WriteOptions(), &batch));
|
||||||
|
if (!status.ok()) GNTHROW (LdbEx, "Ldb: del: " + status.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes the batch. Throws LdbEx if not successfull. */
|
||||||
|
void write (leveldb::WriteBatch& batch, leveldb::WriteOptions options = leveldb::WriteOptions()) {
|
||||||
|
leveldb::Status status (_db->Write (options, &batch));
|
||||||
|
if (!status.ok()) GNTHROW (LdbEx, status.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~Ldb() {
|
||||||
|
_triggers.clear(); // Destroy triggers before closing the database.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace glim
|
||||||
|
|
||||||
|
#endif // _GLIM_LDB_HPP_INCLUDED
|
|
@ -0,0 +1,93 @@
|
||||||
|
|
||||||
|
PREFIX = /usr/local
|
||||||
|
INSTALL2 = ${PREFIX}/include/glim
|
||||||
|
CXXFLAGS = -std=c++1y -Wall -O2 -ggdb -DBOOST_ALL_DYN_LINK
|
||||||
|
|
||||||
|
all: test
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "make test\nmake install\nmake uninstall\nmake clean"
|
||||||
|
|
||||||
|
doc: doxyconf *.hpp
|
||||||
|
mkdir -p doc
|
||||||
|
doxygen doxyconf
|
||||||
|
|
||||||
|
test: test_sqlite test_gstring test_runner test_exception test_ldb
|
||||||
|
|
||||||
|
test_sqlite: bin/test_sqlite
|
||||||
|
cp bin/test_sqlite /tmp/libglim_test_sqlite && chmod +x /tmp/libglim_test_sqlite && /tmp/libglim_test_sqlite && rm -f /tmp/libglim_test_sqlite
|
||||||
|
|
||||||
|
bin/test_sqlite: test_sqlite.cc
|
||||||
|
mkdir -p bin
|
||||||
|
g++ $(CXXFLAGS) test_sqlite.cc -o bin/test_sqlite -lsqlite3
|
||||||
|
|
||||||
|
test_memcache: bin/test_memcache
|
||||||
|
cp bin/test_memcache /tmp/libglim_test_memcache && chmod +x /tmp/libglim_test_memcache && /tmp/libglim_test_memcache && rm -f /tmp/libglim_test_memcache
|
||||||
|
|
||||||
|
bin/test_memcache: test_memcache.cc memcache.hpp
|
||||||
|
mkdir -p bin
|
||||||
|
g++ $(CXXFLAGS) test_memcache.cc -o bin/test_memcache -lmemcache
|
||||||
|
|
||||||
|
bin/test_gstring: test_gstring.cc gstring.hpp
|
||||||
|
mkdir -p bin
|
||||||
|
g++ $(CXXFLAGS) test_gstring.cc -o bin/test_gstring
|
||||||
|
|
||||||
|
test_gstring: bin/test_gstring
|
||||||
|
cp bin/test_gstring /tmp/libglim_test_gstring
|
||||||
|
chmod +x /tmp/libglim_test_gstring
|
||||||
|
/tmp/libglim_test_gstring
|
||||||
|
rm -f /tmp/libglim_test_gstring
|
||||||
|
|
||||||
|
bin/test_runner: test_runner.cc runner.hpp curl.hpp
|
||||||
|
mkdir -p bin
|
||||||
|
g++ $(CXXFLAGS) test_runner.cc -o bin/test_runner -pthread -lboost_log -levent -levent_pthreads -lcurl
|
||||||
|
|
||||||
|
test_runner: bin/test_runner
|
||||||
|
valgrind -q bin/test_runner
|
||||||
|
|
||||||
|
bin/test_exception: test_exception.cc exception.hpp
|
||||||
|
mkdir -p bin
|
||||||
|
g++ $(CXXFLAGS) test_exception.cc -o bin/test_exception -ldl -rdynamic
|
||||||
|
|
||||||
|
test_exception: bin/test_exception
|
||||||
|
valgrind -q bin/test_exception
|
||||||
|
|
||||||
|
test_ldb: test_ldb.cc ldb.hpp
|
||||||
|
mkdir -p bin
|
||||||
|
g++ $(CXXFLAGS) test_ldb.cc -o bin/test_ldb \
|
||||||
|
-lleveldb -lboost_serialization -lboost_filesystem -lboost_system
|
||||||
|
valgrind -q bin/test_ldb
|
||||||
|
|
||||||
|
bin/test_cbcoro: test_cbcoro.cc
|
||||||
|
mkdir -p bin
|
||||||
|
g++ $(CXXFLAGS) test_cbcoro.cc -o bin/test_cbcoro -pthread
|
||||||
|
|
||||||
|
test_cbcoro: bin/test_cbcoro
|
||||||
|
bin/test_cbcoro
|
||||||
|
|
||||||
|
install:
|
||||||
|
mkdir -p ${INSTALL2}/
|
||||||
|
cp sqlite.hpp ${INSTALL2}/
|
||||||
|
cp NsecTimer.hpp ${INSTALL2}/
|
||||||
|
cp TscTimer.hpp ${INSTALL2}/
|
||||||
|
cp memcache.hpp ${INSTALL2}/
|
||||||
|
cp gstring.hpp ${INSTALL2}/
|
||||||
|
cp runner.hpp ${INSTALL2}/
|
||||||
|
cp hget.hpp ${INSTALL2}/
|
||||||
|
cp curl.hpp ${INSTALL2}/
|
||||||
|
cp mdb.hpp ${INSTALL2}/
|
||||||
|
cp ldb.hpp ${INSTALL2}/
|
||||||
|
cp exception.hpp ${INSTALL2}/
|
||||||
|
cp SerializablePool.hpp ${INSTALL2}/
|
||||||
|
cp cbcoro.hpp ${INSTALL2}/
|
||||||
|
cp raii.hpp ${INSTALL2}/
|
||||||
|
cp channel.hpp ${INSTALL2}/
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -rf ${INSTALL2}
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf bin/*
|
||||||
|
rm -rf doc
|
||||||
|
rm -f /tmp/libglim_test_*
|
||||||
|
rm -f *.exe.stackdump
|
|
@ -0,0 +1,499 @@
|
||||||
|
#ifndef _GLIM_MDB_HPP_INCLUDED
|
||||||
|
#define _GLIM_MDB_HPP_INCLUDED
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A C++ wrapper around MDB (http://www.symas.com/mdb/).
|
||||||
|
* @code
|
||||||
|
Copyright 2012 Kozarezov Artem Aleksandrovich
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
* @endcode
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <mdb.h>
|
||||||
|
#include <boost/archive/binary_oarchive.hpp>
|
||||||
|
#include <boost/archive/binary_iarchive.hpp>
|
||||||
|
#include <boost/serialization/serialization.hpp>
|
||||||
|
#include <boost/noncopyable.hpp>
|
||||||
|
#include <boost/iterator/iterator_facade.hpp>
|
||||||
|
#include <boost/range/iterator_range.hpp>
|
||||||
|
|
||||||
|
#include <arpa/inet.h> // htonl, ntohl
|
||||||
|
|
||||||
|
#include "gstring.hpp"
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
struct MdbEx: public std::runtime_error {MdbEx (std::string message): std::runtime_error (message) {}};
|
||||||
|
|
||||||
|
template <typename T> inline void mdbSerialize (gstring& bytes, const T& data) {
|
||||||
|
gstring_stream stream (bytes);
|
||||||
|
boost::archive::binary_oarchive oa (stream, boost::archive::no_header);
|
||||||
|
oa << data;
|
||||||
|
}
|
||||||
|
template <typename V> inline void mdbDeserialize (const gstring& bytes, V& data) {
|
||||||
|
gstring_stream stream (const_cast<gstring&> (bytes));
|
||||||
|
boost::archive::binary_iarchive ia (stream, boost::archive::no_header);
|
||||||
|
ia >> data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** uint32_t keys are stored big-endian (network byte order) in order to be compatible with lexicographic ordering. */
|
||||||
|
template <> inline void mdbSerialize<uint32_t> (gstring& bytes, const uint32_t& ui) {
|
||||||
|
uint32_t nui = htonl (ui); bytes.append ((const char*) &nui, sizeof (uint32_t));}
|
||||||
|
/** Deserialize uint32_t from big-endian (network byte order). */
|
||||||
|
template <> inline void mdbDeserialize<uint32_t> (const gstring& bytes, uint32_t& ui) {
|
||||||
|
if (bytes.size() != sizeof (uint32_t)) throw MdbEx ("Not uint32_t, wrong number of bytes");
|
||||||
|
uint32_t nui = * (uint32_t*) bytes.data(); ui = ntohl (nui);}
|
||||||
|
|
||||||
|
/** If the data is `gstring` then use the data's buffer directly, no copy. */
|
||||||
|
template <> inline void mdbSerialize<gstring> (gstring& bytes, const gstring& data) {
|
||||||
|
bytes = gstring (0, (void*) data.data(), false, data.length());}
|
||||||
|
/** Deserializing into `gstring` copies the bytes into it, reusing its buffer. */
|
||||||
|
template <> inline void mdbDeserialize<gstring> (const gstring& bytes, gstring& data) {
|
||||||
|
data.clear() << bytes;}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header-only C++ wrapper around OpenLDAP-MDB.\n
|
||||||
|
* Uses Boost Serialization to pack keys and values (glim::gstring can be used for raw bytes).\n
|
||||||
|
* Allows semi-automatic indexing with triggers.\n
|
||||||
|
* Known issues: http://www.openldap.org/its/index.cgi?findid=7448
|
||||||
|
*/
|
||||||
|
struct Mdb {
|
||||||
|
std::shared_ptr<MDB_env> _env;
|
||||||
|
MDB_dbi _dbi = 0;
|
||||||
|
|
||||||
|
typedef std::unique_ptr<MDB_txn, void(*)(MDB_txn*)> Transaction;
|
||||||
|
|
||||||
|
/** Holds the current key and value of the Iterator. */
|
||||||
|
struct IteratorEntry {
|
||||||
|
MDB_val _key = {0, 0}, _val = {0, 0};
|
||||||
|
/** Zero-copy view of the current key bytes. Should *not* be used after the Iterator is changed or destroyed. */
|
||||||
|
const gstring keyView() const {return gstring (0, _key.mv_data, false, _key.mv_size, true);} // Zero copy.
|
||||||
|
/** Zero-copy view of the current value bytes. Should *not* be used after the Iterator is changed or destroyed. */
|
||||||
|
const gstring valueView() const {return gstring (0, _val.mv_data, false, _val.mv_size, true);} // Zero copy.
|
||||||
|
/** Deserialize into `key`. */
|
||||||
|
template <typename T> void getKey (T& key) const {mdbDeserialize (keyView(), key);}
|
||||||
|
/** Deserialize the key into a temporary and return it. */
|
||||||
|
template <typename T> T getKey() const {T key; getKey (key); return key;}
|
||||||
|
/** Deserialize into `value`. */
|
||||||
|
template <typename T> void getValue (T& value) const {mdbDeserialize (valueView(), value);}
|
||||||
|
/** Deserialize the value into a temporary and return it. */
|
||||||
|
template <typename T> T getValue() const {T value; getValue (value); return value;}
|
||||||
|
};
|
||||||
|
/** Holds the Iterator's unique transaction and cursor, allowing the Iterator to be copied. */
|
||||||
|
struct IteratorImpl: boost::noncopyable {
|
||||||
|
Mdb* _mdb;
|
||||||
|
Transaction _txn;
|
||||||
|
MDB_cursor* _cur;
|
||||||
|
IteratorImpl (Mdb* mdb, Transaction&& txn, MDB_cursor* cur): _mdb (mdb), _txn (std::move (txn)), _cur (cur) {}
|
||||||
|
~IteratorImpl() {
|
||||||
|
if (_cur) {::mdb_cursor_close (_cur); _cur = nullptr;}
|
||||||
|
if (_mdb && _txn) {_mdb->commitTransaction (_txn); _mdb = nullptr;}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** Wraps MDB cursor and cursor's transaction. */
|
||||||
|
struct Iterator: public boost::iterator_facade<Iterator, IteratorEntry, boost::bidirectional_traversal_tag> {
|
||||||
|
std::shared_ptr<IteratorImpl> _impl; // Iterator might be copied around, thus we keep the unique things in IteratorImpl.
|
||||||
|
IteratorEntry _entry;
|
||||||
|
bool _stayInKey = false;
|
||||||
|
|
||||||
|
Iterator (const Iterator&) = default;
|
||||||
|
Iterator (Iterator&&) = default;
|
||||||
|
/** Iterate from the beginning or the end of the database.
|
||||||
|
* @param position can be MDB_FIRST or MDB_LAST */
|
||||||
|
Iterator (Mdb* mdb, int position = 0): _stayInKey (false) {
|
||||||
|
Transaction txn (mdb->beginTransaction());
|
||||||
|
MDB_cursor* cur = nullptr; int rc = ::mdb_cursor_open (txn.get(), mdb->_dbi, &cur);
|
||||||
|
if (rc) throw MdbEx ("mdb_cursor_open");
|
||||||
|
_impl = std::make_shared<IteratorImpl> (mdb, std::move (txn), cur);
|
||||||
|
if (position == ::MDB_FIRST || position == ::MDB_LAST) {
|
||||||
|
rc = ::mdb_cursor_get (cur, &_entry._key, &_entry._val, (MDB_cursor_op) position);
|
||||||
|
if (rc) throw MdbEx ("mdb_cursor_get");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Iterate over `key` values.
|
||||||
|
* @param stayInKey if `false` then iterator can go farther than the `key`. */
|
||||||
|
Iterator (Mdb* mdb, const gstring& key, bool stayInKey = true): _stayInKey (stayInKey) {
|
||||||
|
Transaction txn (mdb->beginTransaction());
|
||||||
|
MDB_cursor* cur = nullptr; int rc = ::mdb_cursor_open (txn.get(), mdb->_dbi, &cur);
|
||||||
|
if (rc) throw MdbEx ("mdb_cursor_open");
|
||||||
|
_impl = std::make_shared<IteratorImpl> (mdb, std::move (txn), cur);
|
||||||
|
_entry._key = {key.size(), (void*) key.data()};
|
||||||
|
rc = ::mdb_cursor_get (cur, &_entry._key, &_entry._val, ::MDB_SET_KEY);
|
||||||
|
if (rc == MDB_NOTFOUND) {_entry._key = {0, 0}; _entry._val = {0, 0};}
|
||||||
|
else if (rc) throw MdbEx ("mdb_cursor_get");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EndIteratorFlag {};
|
||||||
|
/** The "end" iterator does not open an MDB transaction (this is essential for having a pair of iterators without a deadlock). */
|
||||||
|
Iterator (EndIteratorFlag): _stayInKey (false) {}
|
||||||
|
/** True if the iterator isn't pointing anywhere. */
|
||||||
|
bool end() const {return _entry._key.mv_size == 0;}
|
||||||
|
|
||||||
|
bool equal (const Iterator& other) const {
|
||||||
|
IteratorImpl* impl = _impl.get();
|
||||||
|
if (mdb_cmp (impl->_txn.get(), impl->_mdb->_dbi, &_entry._key, &other._entry._key)) return false;
|
||||||
|
if (mdb_dcmp (impl->_txn.get(), impl->_mdb->_dbi, &_entry._val, &other._entry._val)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
IteratorEntry& dereference() const {
|
||||||
|
// NB: Boost iterator_facade expects the `dereference` to be a `const` method.
|
||||||
|
// I guess Iterator is not modified, so the `dereference` is `const`, even though the Entry can be modified.
|
||||||
|
return const_cast<IteratorEntry&> (_entry);}
|
||||||
|
void increment() {
|
||||||
|
int rc = ::mdb_cursor_get (_impl->_cur, &_entry._key, &_entry._val, _stayInKey ? ::MDB_NEXT_DUP : ::MDB_NEXT);
|
||||||
|
if (rc) {_entry._key = {0,0}; _entry._val = {0,0};}
|
||||||
|
}
|
||||||
|
void decrement() {
|
||||||
|
int rc = ::mdb_cursor_get (_impl->_cur, &_entry._key, &_entry._val, _stayInKey ? ::MDB_PREV_DUP : ::MDB_PREV);
|
||||||
|
if (rc) {_entry._key = {0,0}; _entry._val = {0,0};}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Iterator begin() {return Iterator (this, ::MDB_FIRST);}
|
||||||
|
const Iterator end() {return Iterator (Iterator::EndIteratorFlag());}
|
||||||
|
/** Position the cursor at the first `key` record.\n
|
||||||
|
* The iterator increment will use `MDB_NEXT_DUP`, staying withing the `key`.\n
|
||||||
|
* See also the `all` method. */
|
||||||
|
template <typename K> Iterator values (const K& key) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
mdbSerialize (kbytes, key);
|
||||||
|
return Iterator (this, kbytes);
|
||||||
|
}
|
||||||
|
/** Range over the `key` values.\n
|
||||||
|
* See also the `all` method. */
|
||||||
|
template <typename K> boost::iterator_range<Iterator> valuesRange (const K& key) {return boost::iterator_range<Iterator> (values (key), end());}
|
||||||
|
|
||||||
|
struct Trigger {
|
||||||
|
virtual gstring getTriggerName() const {return C2GSTRING ("defaultTriggerName");};
|
||||||
|
virtual void add (Mdb& mdb, void* key, gstring& kbytes, void* value, gstring& vbytes, Transaction& txn) = 0;
|
||||||
|
virtual void erase (Mdb& mdb, void* key, gstring& kbytes, Transaction& txn) = 0;
|
||||||
|
virtual void eraseKV (Mdb& mdb, void* key, gstring& kbytes, void* value, gstring& vbytes, Transaction& txn) = 0;
|
||||||
|
};
|
||||||
|
std::map<gstring, std::shared_ptr<Trigger>> _triggers;
|
||||||
|
|
||||||
|
void setTrigger (std::shared_ptr<Trigger> trigger) {
|
||||||
|
_triggers[trigger->getTriggerName()] = trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `flags` can be `MDB_RDONLY` */
|
||||||
|
Transaction beginTransaction (unsigned flags = 0) {
|
||||||
|
MDB_txn* txn = 0; int rc = ::mdb_txn_begin (_env.get(), nullptr, flags, &txn);
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_txn_begin: ") + ::strerror (rc));
|
||||||
|
return Transaction (txn, ::mdb_txn_abort);
|
||||||
|
}
|
||||||
|
void commitTransaction (Transaction& txn) {
|
||||||
|
int rc = ::mdb_txn_commit (txn.get());
|
||||||
|
txn.release(); // Must prevent `mdb_txn_abort` from happening (even if rc != 0).
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_txn_commit: ") + ::strerror (rc));
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual unsigned envFlags (uint8_t sync) {
|
||||||
|
unsigned flags = MDB_NOSUBDIR;
|
||||||
|
if (sync < 1) flags |= MDB_NOSYNC; else if (sync < 2) flags |= MDB_NOMETASYNC;
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
/** Used before `mdb_env_open`. By default sets the number of database to 32. */
|
||||||
|
virtual void envConf (MDB_env* env) {
|
||||||
|
int rc = ::mdb_env_set_maxdbs (env, 32);
|
||||||
|
if (rc) throw MdbEx (std::string ("envConf: ") + ::strerror (rc));
|
||||||
|
}
|
||||||
|
virtual void dbFlags (unsigned& flags) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void open (const char* dbName, bool dup) {
|
||||||
|
auto txn = beginTransaction();
|
||||||
|
unsigned flags = MDB_CREATE;
|
||||||
|
if (dup) flags |= MDB_DUPSORT;
|
||||||
|
dbFlags (flags);
|
||||||
|
int rc = ::mdb_open (txn.get(), dbName, flags, &_dbi);
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_open (") + dbName + "): " + ::strerror (rc));
|
||||||
|
commitTransaction (txn);
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
|
||||||
|
/** Opens MDB environment and MDB database. */
|
||||||
|
Mdb (const char* path, size_t maxSizeMb = 1024, const char* dbName = "main", uint8_t sync = 0, bool dup = true, mode_t mode = 0660) {
|
||||||
|
MDB_env* env = 0; int rc = ::mdb_env_create (&env);
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_env_create: ") + ::strerror (rc));
|
||||||
|
_env.reset (env, ::mdb_env_close);
|
||||||
|
rc = ::mdb_env_set_mapsize (env, maxSizeMb * 1024 * 1024);
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_env_set_mapsize: ") + ::strerror (rc));
|
||||||
|
envConf (env);
|
||||||
|
rc = ::mdb_env_open (env, path, envFlags (sync), mode);
|
||||||
|
_dbi = 0; open (dbName, dup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens MDB database in the provided environment. */
|
||||||
|
Mdb (std::shared_ptr<MDB_env> env, const char* dbName, bool dup = true): _env (env), _dbi (0) {
|
||||||
|
open (dbName, dup);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename K, typename V> void add (const K& key, const V& value, Transaction& txn) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
mdbSerialize (kbytes, key);
|
||||||
|
MDB_val mkey = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
|
||||||
|
char vbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring vbytes (sizeof (vbuf), vbuf, false, 0);
|
||||||
|
mdbSerialize (vbytes, value);
|
||||||
|
MDB_val mvalue = {vbytes.size(), (void*) vbytes.data()};
|
||||||
|
|
||||||
|
for (auto& trigger: _triggers) trigger.second->add (*this, (void*) &key, kbytes, (void*) &value, vbytes, txn);
|
||||||
|
|
||||||
|
int rc = ::mdb_put (txn.get(), _dbi, &mkey, &mvalue, 0);
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_put: ") + ::strerror (rc));
|
||||||
|
}
|
||||||
|
template <typename K, typename V> void add (const K& key, const V& value) {
|
||||||
|
Transaction txn (beginTransaction());
|
||||||
|
add (key, value, txn);
|
||||||
|
commitTransaction (txn);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename K, typename V> bool first (const K& key, V& value, Transaction& txn) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
mdbSerialize (kbytes, key);
|
||||||
|
MDB_val mkey = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
MDB_val mvalue;
|
||||||
|
int rc = ::mdb_get (txn.get(), _dbi, &mkey, &mvalue);
|
||||||
|
if (rc == MDB_NOTFOUND) return false;
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_get: ") + ::strerror (rc));
|
||||||
|
gstring vstr (0, mvalue.mv_data, false, mvalue.mv_size);
|
||||||
|
mdbDeserialize (vstr, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
template <typename K, typename V> bool first (const K& key, V& value) {
|
||||||
|
Transaction txn (beginTransaction (MDB_RDONLY));
|
||||||
|
bool rb = first (key, value, txn);
|
||||||
|
commitTransaction (txn);
|
||||||
|
return rb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Iterate over `key` values until `visitor` returns `false`. Return the number of values visited. */
|
||||||
|
template <typename K, typename V> int32_t all (const K& key, std::function<bool(const V&)> visitor, Transaction& txn) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
mdbSerialize (kbytes, key);
|
||||||
|
MDB_val mkey = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
|
||||||
|
MDB_cursor* cur = 0; int rc = ::mdb_cursor_open (txn.get(), _dbi, &cur);
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_cursor_open: ") + ::strerror (rc));
|
||||||
|
std::unique_ptr<MDB_cursor, void(*)(MDB_cursor*)> curHolder (cur, ::mdb_cursor_close);
|
||||||
|
MDB_val mval = {0, 0};
|
||||||
|
rc = ::mdb_cursor_get (cur, &mkey, &mval, ::MDB_SET_KEY); if (rc == MDB_NOTFOUND) return 0;
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_cursor_get: ") + ::strerror (rc));
|
||||||
|
|
||||||
|
V value;
|
||||||
|
gstring vstr (0, mval.mv_data, false, mval.mv_size);
|
||||||
|
mdbDeserialize (vstr, value);
|
||||||
|
bool goOn = visitor (value);
|
||||||
|
int32_t count = 1;
|
||||||
|
|
||||||
|
while (goOn) {
|
||||||
|
rc = ::mdb_cursor_get (cur, &mkey, &mval, ::MDB_NEXT_DUP); if (rc == MDB_NOTFOUND) return count;
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_cursor_get: ") + ::strerror (rc));
|
||||||
|
gstring vstr (0, mval.mv_data, false, mval.mv_size);
|
||||||
|
mdbDeserialize (vstr, value);
|
||||||
|
goOn = visitor (value);
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
/** Iterate over `key` values until `visitor` returns `false`. Return the number of values visited. */
|
||||||
|
template <typename K, typename V> int32_t all (const K& key, std::function<bool(const V&)> visitor) {
|
||||||
|
Transaction txn (beginTransaction (MDB_RDONLY));
|
||||||
|
int32_t count = all (key, visitor, txn);
|
||||||
|
commitTransaction (txn);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename K, typename V> bool eraseKV (const K& key, const V& value, Transaction& txn) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
mdbSerialize (kbytes, key);
|
||||||
|
if (kbytes.empty()) throw MdbEx ("eraseKV: key is empty");
|
||||||
|
MDB_val mkey = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
|
||||||
|
char vbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring vbytes (sizeof (vbuf), vbuf, false, 0);
|
||||||
|
mdbSerialize (vbytes, value);
|
||||||
|
MDB_val mvalue = {vbytes.size(), (void*) vbytes.data()};
|
||||||
|
|
||||||
|
for (auto& trigger: _triggers) trigger.second->eraseKV (*this, (void*) &key, kbytes, (void*) &value, vbytes, txn);
|
||||||
|
|
||||||
|
int rc = ::mdb_del (txn.get(), _dbi, &mkey, &mvalue);
|
||||||
|
if (rc == MDB_NOTFOUND) return false;
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_del: ") + ::strerror (rc));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
template <typename K, typename V> bool eraseKV (const K& key, const V& value) {
|
||||||
|
Transaction txn (beginTransaction());
|
||||||
|
bool rb = eraseKV (key, value, txn);
|
||||||
|
commitTransaction (txn);
|
||||||
|
return rb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erase all values of the `key`. */
|
||||||
|
template <typename K> bool erase (const K& key, Transaction& txn) {
|
||||||
|
char kbuf[64]; // Allow up to 64 bytes to be serialized without heap allocations.
|
||||||
|
gstring kbytes (sizeof (kbuf), kbuf, false, 0);
|
||||||
|
mdbSerialize (kbytes, key);
|
||||||
|
if (kbytes.empty()) throw MdbEx ("erase: key is empty");
|
||||||
|
MDB_val mkey = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
|
||||||
|
for (auto& trigger: _triggers) trigger.second->erase (*this, (void*) &key, kbytes, txn);
|
||||||
|
|
||||||
|
int rc = ::mdb_del (txn.get(), _dbi, &mkey, nullptr);
|
||||||
|
if (rc == MDB_NOTFOUND) return false;
|
||||||
|
if (rc) throw MdbEx (std::string ("mdb_del: ") + ::strerror (rc));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/** Erase all values of the `key`. */
|
||||||
|
template <typename K> bool erase (const K& key) {
|
||||||
|
Transaction txn (beginTransaction());
|
||||||
|
bool rb = erase (key, txn);
|
||||||
|
commitTransaction (txn);
|
||||||
|
return rb;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test (Mdb& mdb) {
|
||||||
|
mdb.add (std::string ("foo"), std::string ("bar"));
|
||||||
|
// NB: "MDB_DUPSORT doesn't allow duplicate duplicates" (Howard Chu)
|
||||||
|
mdb.add (std::string ("foo"), std::string ("bar"));
|
||||||
|
mdb.add ((uint32_t) 123, 1);
|
||||||
|
mdb.add ((uint32_t) 123, 2);
|
||||||
|
mdb.add (C2GSTRING ("foo"), 3);
|
||||||
|
mdb.add (C2GSTRING ("foo"), 4);
|
||||||
|
mdb.add (C2GSTRING ("gsk"), C2GSTRING ("gsv"));
|
||||||
|
string ts; int ti; gstring tgs;
|
||||||
|
auto fail = [](string msg) {throw std::runtime_error ("assertion failed: " + msg);};
|
||||||
|
if (!mdb.first (std::string ("foo"), ts) || ts != "bar") fail ("!foo=bar");
|
||||||
|
if (!mdb.first ((uint32_t) 123, ti) || ti < 1 || ti > 2) fail ("!123");
|
||||||
|
if (!mdb.first (C2GSTRING ("foo"), ti) || ti < 3 || ti > 4) fail ("!foo=3,4");
|
||||||
|
if (!mdb.first (C2GSTRING ("gsk"), tgs) || tgs != "gsv") fail ("!gsk=gsv");
|
||||||
|
|
||||||
|
// Test range-based for.
|
||||||
|
int count = 0; bool haveGskGsv = false;
|
||||||
|
for (auto&& entry: mdb) {
|
||||||
|
if (!entry._key.mv_size || !entry._val.mv_size) fail ("!entry");
|
||||||
|
if (entry.keyView() == "gsk") {
|
||||||
|
if (entry.getKey<gstring>() != "gsk") fail ("getKey(gsk)!=gsk");
|
||||||
|
if (entry.getValue<gstring>() != "gsv") fail ("getValue(gsk)!=gsv");
|
||||||
|
haveGskGsv = true;
|
||||||
|
}
|
||||||
|
++count;}
|
||||||
|
if (count != 6) fail ("count!=6"); // foo=bar, 123=1, 123=2, foo=3, foo=4, gsk=gsv
|
||||||
|
if (!haveGskGsv) fail ("!haveGskGsv");
|
||||||
|
|
||||||
|
// Test `values`.
|
||||||
|
count = 0; int sum = 0;
|
||||||
|
for (auto&& entry: mdb.valuesRange ((uint32_t) 123)) {
|
||||||
|
if (entry.getKey<uint32_t>() != (uint32_t) 123) fail("values(123).key!=123");
|
||||||
|
++count; sum += entry.getValue<int>();
|
||||||
|
}
|
||||||
|
if (count != 2) fail ("count(123)!=2");
|
||||||
|
if (sum != 3) fail ("sum(123)!=3");
|
||||||
|
|
||||||
|
if (!mdb.eraseKV ((uint32_t) 123, 1)) fail ("!eraseKV(123,1)");
|
||||||
|
if (!mdb.first ((uint32_t) 123, ti) || ti != 2) fail ("!123=2");
|
||||||
|
if (!mdb.eraseKV ((uint32_t) 123, 2)) fail ("!eraseKV(123,2)");
|
||||||
|
if (mdb.first ((uint32_t) 123, ti)) fail ("123");
|
||||||
|
|
||||||
|
if (!mdb.erase (C2GSTRING ("foo"))) fail ("!erase(g(foo))");
|
||||||
|
if (mdb.first (C2GSTRING ("foo"), ti)) fail ("foo");
|
||||||
|
if (!mdb.erase (std::string ("foo"))) fail ("!erase(str(foo))");
|
||||||
|
|
||||||
|
{ // We've erased "123" and "foo", the only key left is "gsk" (gsk=gsv), let's test the iterator boundaries on this small dataset.
|
||||||
|
auto&& it = mdb.begin();
|
||||||
|
if (it->getKey<gstring>() != "gsk") fail ("first key !gsk " + it->keyView().str());
|
||||||
|
if (!(++it).end()) fail ("++it != end");
|
||||||
|
if ((--it)->getKey<gstring>() != "gsk") fail ("can't go back to gsk");
|
||||||
|
if (!(--it).end()) fail ("--it != end");
|
||||||
|
if ((++it)->getKey<gstring>() != "gsk") fail ("can't go forward to gsk");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimpleIndexTrigger: public Trigger {
|
||||||
|
const char* _name; Mdb _indexDb;
|
||||||
|
SimpleIndexTrigger (Mdb& mdb, const char* name = "index"): _name (name), _indexDb (mdb._env, name) {}
|
||||||
|
gstring getTriggerName() {return gstring (0, (void*) _name, false, strlen (_name), true);}
|
||||||
|
void add (Mdb& mdb, void* key, gstring& kbytes, void* value, gstring& vbytes, Transaction& txn) {
|
||||||
|
MDB_val mkey = {vbytes.size(), (void*) vbytes.data()};
|
||||||
|
MDB_val mvalue = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
int rc = ::mdb_put (txn.get(), _indexDb._dbi, &mkey, &mvalue, 0);
|
||||||
|
if (rc) throw MdbEx (std::string ("index, mdb_put: ") + ::strerror (rc));
|
||||||
|
}
|
||||||
|
void erase (Mdb& mdb, void* ekey, gstring& kbytes, Transaction& txn) {
|
||||||
|
// Get all the values and remove them from the index.
|
||||||
|
MDB_cursor* cur = 0; int rc = ::mdb_cursor_open (txn.get(), mdb._dbi, &cur);
|
||||||
|
if (rc) throw MdbEx (std::string ("index, erase, mdb_cursor_open: ") + ::strerror (rc));
|
||||||
|
std::unique_ptr<MDB_cursor, void(*)(MDB_cursor*)> curHolder (cur, ::mdb_cursor_close);
|
||||||
|
MDB_val mkey = {kbytes.size(), (void*) kbytes.data()}, val = {0, 0};
|
||||||
|
rc = ::mdb_cursor_get (cur, &mkey, &val, ::MDB_SET_KEY); if (rc == MDB_NOTFOUND) return;
|
||||||
|
if (rc) throw MdbEx (std::string ("index, erase, mdb_cursor_get: ") + ::strerror (rc));
|
||||||
|
rc = ::mdb_del (txn.get(), _indexDb._dbi, &val, &mkey);
|
||||||
|
if (rc && rc != MDB_NOTFOUND) throw MdbEx (std::string ("index, erase, mdb_del: ") + ::strerror (rc));
|
||||||
|
for (;;) {
|
||||||
|
rc = ::mdb_cursor_get (cur, &mkey, &val, ::MDB_NEXT_DUP); if (rc == MDB_NOTFOUND) return;
|
||||||
|
if (rc) throw MdbEx (std::string ("index, erase, mdb_cursor_get: ") + ::strerror (rc));
|
||||||
|
rc = ::mdb_del (txn.get(), _indexDb._dbi, &val, &mkey);
|
||||||
|
if (rc && rc != MDB_NOTFOUND) throw MdbEx (std::string ("index, erase, mdb_del: ") + ::strerror (rc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void eraseKV (Mdb& mdb, void* key, gstring& kbytes, void* value, gstring& vbytes, Transaction& txn) {
|
||||||
|
MDB_val mkey = {vbytes.size(), (void*) vbytes.data()};
|
||||||
|
MDB_val mvalue = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
int rc = ::mdb_del (txn.get(), _indexDb._dbi, &mkey, &mvalue);
|
||||||
|
if (rc && rc != MDB_NOTFOUND) throw MdbEx (std::string ("index, mdb_del: ") + ::strerror (rc));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto indexTrigger = std::make_shared<SimpleIndexTrigger> (mdb); mdb.setTrigger (indexTrigger); auto& indexDb = indexTrigger->_indexDb;
|
||||||
|
mdb.erase (C2GSTRING ("gsk")); // NB: "gsk" wasn't indexed here. `IndexTrigger.erase` should handle this gracefully.
|
||||||
|
|
||||||
|
// Add indexed.
|
||||||
|
mdb.add (C2GSTRING ("ik"), C2GSTRING ("iv1"));
|
||||||
|
mdb.add (C2GSTRING ("ik"), string ("iv2"));
|
||||||
|
mdb.add (C2GSTRING ("ik"), 3);
|
||||||
|
// Check the index.
|
||||||
|
gstring ik;
|
||||||
|
if (!indexDb.first (C2GSTRING ("iv1"), ik) || ik != "ik") fail ("!iv1=ik");
|
||||||
|
if (!indexDb.first (string ("iv2"), ik) || ik != "ik") fail ("!iv2=ik");
|
||||||
|
if (!indexDb.first (3, ik) || ik != "ik") fail ("!iv3=ik");
|
||||||
|
|
||||||
|
// Remove indexed.
|
||||||
|
mdb.eraseKV (C2GSTRING ("ik"), string ("iv2"));
|
||||||
|
// Check the index.
|
||||||
|
if (!indexDb.first (C2GSTRING ("iv1"), ik) || ik != "ik") fail ("!iv1=ik");
|
||||||
|
if (indexDb.first (string ("iv2"), ik)) fail ("iv2=ik");
|
||||||
|
if (!indexDb.first (3, ik) || ik != "ik") fail ("!iv3=ik");
|
||||||
|
|
||||||
|
// Remove indexed.
|
||||||
|
mdb.erase (C2GSTRING ("ik"));
|
||||||
|
// Check the index.
|
||||||
|
if (indexDb.first (C2GSTRING ("iv1"), ik)) fail ("iv1");
|
||||||
|
if (indexDb.first (3, ik)) fail ("iv3");
|
||||||
|
// Check the data.
|
||||||
|
if (mdb.first (C2GSTRING ("ik"), ik)) fail ("ik");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~Mdb() {
|
||||||
|
_triggers.clear(); // Destroy triggers before closing the database.
|
||||||
|
if (_dbi) {::mdb_close (_env.get(), _dbi); _dbi = 0;}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace glim
|
||||||
|
|
||||||
|
#endif // _GLIM_MDB_HPP_INCLUDED
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,34 @@
|
||||||
|
#include <functional>
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
// http://stackoverflow.com/questions/2121607/any-raii-template-in-boost-or-c0x/
|
||||||
|
|
||||||
|
/// RAII helper. Keeps the functor and runs it in the destructor.
|
||||||
|
/// Example: \code auto unmap = raiiFun ([&]() {munmap (fd, size);}); \endcode
|
||||||
|
template<typename Fun> struct RAIIFun {
|
||||||
|
Fun _fun;
|
||||||
|
RAIIFun (RAIIFun&&) = default;
|
||||||
|
RAIIFun (const RAIIFun&) = default;
|
||||||
|
template<typename FunArg> RAIIFun (FunArg&& fun): _fun (std::forward<Fun> (fun)) {}
|
||||||
|
~RAIIFun() {_fun();}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The idea to name it `finally` comes from http://www.codeproject.com/Tips/476970/finally-clause-in-Cplusplus.
|
||||||
|
/// Example: \code finally unmap ([&]() {munmap (fd, size);}); \endcode
|
||||||
|
typedef RAIIFun<std::function<void(void)>> finally;
|
||||||
|
|
||||||
|
/// Runs the given functor when going out of scope.
|
||||||
|
/// Example: \code
|
||||||
|
/// auto closeFd = raiiFun ([&]() {close (fd);});
|
||||||
|
/// auto unmap = raiiFun ([&]() {munmap (fd, size);});
|
||||||
|
/// \endcode
|
||||||
|
template<typename Fun> RAIIFun<Fun> raiiFun (const Fun& fun) {return RAIIFun<Fun> (fun);}
|
||||||
|
|
||||||
|
/// Runs the given functor when going out of scope.
|
||||||
|
/// Example: \code
|
||||||
|
/// auto closeFd = raiiFun ([&]() {close (fd);});
|
||||||
|
/// auto unmap = raiiFun ([&]() {munmap (fd, size);});
|
||||||
|
/// \endcode
|
||||||
|
template<typename Fun> RAIIFun<Fun> raiiFun (Fun&& fun) {return RAIIFun<Fun> (std::move (fun));}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,402 @@
|
||||||
|
#ifndef _GLIM_RUNNER_INCLUDED
|
||||||
|
#define _GLIM_RUNNER_INCLUDED
|
||||||
|
|
||||||
|
#include <algorithm> // min
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <chrono>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <event2/event.h> // cf. hiperfifo.cpp at http://article.gmane.org/gmane.comp.web.curl.library/37752
|
||||||
|
|
||||||
|
#include <boost/intrusive_ptr.hpp>
|
||||||
|
#include <boost/lockfree/queue.hpp> // http://www.boost.org/doc/libs/1_53_0/doc/html/boost/lockfree/queue.html
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdlib.h> // rand
|
||||||
|
#include <sys/eventfd.h>
|
||||||
|
|
||||||
|
#include "gstring.hpp"
|
||||||
|
#include "exception.hpp"
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
/// Listens to messages returned by `curl_multi_info_read`.
|
||||||
|
/// NB: When CURL is queued with `addToCURLM` the CURL's `CURLOPT_PRIVATE` must point to the instance of `CurlmInformationListener`.
|
||||||
|
struct CurlmInformationListener {
|
||||||
|
enum FreeOptions {REMOVE_CURL_FROM_CURLM = 1, CURL_CLEANUP = 2, DELETE_LISTENER = 4, REMOVE_CLEAN_DELETE = 1|2|4};
|
||||||
|
virtual FreeOptions information (CURLMsg*, CURLM*) = 0;
|
||||||
|
virtual ~CurlmInformationListener() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Listener deferring to a lambda.
|
||||||
|
struct FunCurlmLisneter: public glim::CurlmInformationListener {
|
||||||
|
std::function <void(CURLMsg*, CURLM*)> _fun;
|
||||||
|
FreeOptions _freeOptions;
|
||||||
|
FunCurlmLisneter (std::function <void(CURLMsg*, CURLM*)>&& fun, FreeOptions freeOptions): _fun (std::move (fun)), _freeOptions (freeOptions) {}
|
||||||
|
virtual FreeOptions information (CURLMsg* msg, CURLM* curlm) override {
|
||||||
|
if (__builtin_expect ((bool) _fun, 1))
|
||||||
|
try {_fun (msg, curlm);} catch (const std::exception& ex) {BOOST_LOG_TRIVIAL (error) << "FunCurlmLisneter] " << ex.what();}
|
||||||
|
return _freeOptions;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Running cURL jobs in a single thread.
|
||||||
|
/// NB: The RunnerV2 *must* be allocated with `boost::intrusive_ptr` (typically you'd use `RunnerV2::instance()`).
|
||||||
|
class RunnerV2 {
|
||||||
|
std::atomic_int_fast32_t _references {0}; // For intrusive_ptr.
|
||||||
|
CURLM* _multi = nullptr; ///< Initialized in `run`. Should not be used outside of it.
|
||||||
|
int _eventFd = 0; ///< Used to give the `curl_multi_wait` some work when there's no cURL descriptors and to wake it from `withCURLM`.
|
||||||
|
boost::lockfree::queue<CURL*, boost::lockfree::capacity<64>> _queue; ///< `CURL` handles waiting to be added to `CURL_MULTI`.
|
||||||
|
std::thread _thread;
|
||||||
|
std::atomic_bool _running {false}; /// True if the `_thread` is running.
|
||||||
|
|
||||||
|
using FreeOptions = CurlmInformationListener::FreeOptions;
|
||||||
|
|
||||||
|
friend inline void intrusive_ptr_add_ref (RunnerV2*);
|
||||||
|
friend inline void intrusive_ptr_release (RunnerV2*);
|
||||||
|
|
||||||
|
void run() noexcept {
|
||||||
|
try {
|
||||||
|
if (__builtin_expect (_references <= 0, 0)) GTHROW ("RunnerV2] Must be allocated with boost::intrusive_ptr!");
|
||||||
|
_running = true; // NB: _running only becomes true if we're in the intrusive_ptr. ^^
|
||||||
|
pthread_setname_np (pthread_self(), "Runner");
|
||||||
|
_multi = curl_multi_init(); if (__builtin_expect (_multi == nullptr, 0)) GTHROW ("!curl_multi_init");
|
||||||
|
_eventFd = eventfd (0, EFD_CLOEXEC | EFD_NONBLOCK); // Used to pause `curl_multi_wait` when there's no other jobs.
|
||||||
|
if (__builtin_expect (_eventFd == -1, 0)) GTHROW (std::string ("eventfd: ") + ::strerror (errno));
|
||||||
|
while (__builtin_expect (_references > 0, 0)) {
|
||||||
|
// Reset the CURL_EVENT_FD value to 0, so that the `curl_multi_wait` can sleep.
|
||||||
|
if (__builtin_expect (_eventFd > 0, 1)) {eventfd_t count = 0; eventfd_read (_eventFd, &count);}
|
||||||
|
|
||||||
|
// Add the queued CURL handles to our CURLM.
|
||||||
|
CURL* easy = nullptr; while (_queue.pop (easy)) curl_multi_add_handle (_multi, easy);
|
||||||
|
|
||||||
|
// Run the cURL.
|
||||||
|
int runningHandles = 0;
|
||||||
|
CURLMcode rc = curl_multi_perform (_multi, &runningHandles); // http://curl.haxx.se/libcurl/c/curl_multi_perform.html
|
||||||
|
if (__builtin_expect (rc != CURLM_OK, 0)) BOOST_LOG_TRIVIAL (error) << "Runner] curl_multi_perform: " << curl_multi_strerror (rc);
|
||||||
|
|
||||||
|
// Process the finished handles.
|
||||||
|
for (;;) {
|
||||||
|
int messagesLeft = 0; CURLMsg* msg = curl_multi_info_read (_multi, &messagesLeft); if (msg) try {
|
||||||
|
CURL* curl = msg->easy_handle; CurlmInformationListener* listener = 0;
|
||||||
|
if (__builtin_expect (curl_easy_getinfo (curl, CURLINFO_PRIVATE, &listener) == CURLE_OK, 1)) {
|
||||||
|
using FOP = CurlmInformationListener::FreeOptions;
|
||||||
|
FOP fop = listener->information (msg, _multi);
|
||||||
|
if (fop & FOP::REMOVE_CURL_FROM_CURLM) curl_multi_remove_handle (_multi, curl);
|
||||||
|
if (fop & FOP::CURL_CLEANUP) curl_easy_cleanup (curl);
|
||||||
|
if (fop & FOP::DELETE_LISTENER) delete listener;
|
||||||
|
} else {
|
||||||
|
curl_multi_remove_handle (_multi, curl);
|
||||||
|
curl_easy_cleanup (curl);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& ex) {BOOST_LOG_TRIVIAL (error) << "Runner] " << ex.what();}
|
||||||
|
if (messagesLeft == 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait on the cURL file descriptors.
|
||||||
|
int descriptors = 0;
|
||||||
|
curl_waitfd waitfd = {_eventFd, CURL_WAIT_POLLIN, 0};
|
||||||
|
eventfd_t eValue = 0; eventfd_read (_eventFd, &eValue); // Reset the curlEventFd value to zero.
|
||||||
|
rc = curl_multi_wait (_multi, &waitfd, 1, 100, &descriptors); // http://curl.haxx.se/libcurl/c/curl_multi_wait.html
|
||||||
|
if (__builtin_expect (rc != CURLM_OK, 0)) BOOST_LOG_TRIVIAL (error) << "Runner] curl_multi_wait: " << curl_multi_strerror (rc);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& ex) {BOOST_LOG_TRIVIAL (error) << "Runner] " << ex.what();}
|
||||||
|
// Delayed destruction: when we're in intrusive_ptr (_running == true) but no longer referenced.
|
||||||
|
if (_running && _references == 0) delete this; // http://www.parashift.com/c++-faq-lite/delete-this.html
|
||||||
|
else _running = false;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
RunnerV2() {
|
||||||
|
// Start a thread using CURLM in a thread-safe way (that is, from this single thread only).
|
||||||
|
// NB: Handles *can* be passed between threads: http://article.gmane.org/gmane.comp.web.curl.library/33188
|
||||||
|
_thread = std::thread (&RunnerV2::run, this);
|
||||||
|
}
|
||||||
|
~RunnerV2() {
|
||||||
|
_thread.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A singletone instance of the Runner used in order for different programes to reuse the same cURL thread.
|
||||||
|
static boost::intrusive_ptr<RunnerV2>& instance() {
|
||||||
|
static boost::intrusive_ptr<RunnerV2> INSTANCE (new RunnerV2());
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule a CURL handler to be executed in the cURL thread.
|
||||||
|
/// NB: If the handle have a `CURLOPT_PRIVATE` option then it MUST point to an instance of `CurlmInformationListener`.
|
||||||
|
void addToCURLM (CURL* easyHandle) {
|
||||||
|
if (__builtin_expect (!_queue.push (easyHandle), 0)) GTHROW ("Can't push CURL* into the queue.");
|
||||||
|
if (__builtin_expect (_eventFd > 0, 1)) eventfd_write (_eventFd, 1); // Will wake the `curl_multi_wait` up, in order to run the `curl_multi_add_handle`.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule a CURL handler to be executed in the cURL thread.
|
||||||
|
/// NB: `CURLOPT_PRIVATE` is overwritten with a pointer to `FunCurlmLisneter`.
|
||||||
|
void addToCURLM (CURL* easyHandle, std::function <void(CURLMsg*, CURLM*)>&& listener,
|
||||||
|
FreeOptions freeOptions = static_cast<FreeOptions> (FreeOptions::REMOVE_CURL_FROM_CURLM | FreeOptions::DELETE_LISTENER)) {
|
||||||
|
FunCurlmLisneter* funListener = new FunCurlmLisneter (std::move (listener), freeOptions); // Will be deleted by the Runner.
|
||||||
|
curl_easy_setopt (easyHandle, CURLOPT_PRIVATE, funListener); // Tells `addToCURLM` to call this listener later.
|
||||||
|
addToCURLM (easyHandle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void intrusive_ptr_add_ref (RunnerV2* runner) {++ runner->_references;}
|
||||||
|
inline void intrusive_ptr_release (RunnerV2* runner) {if (-- runner->_references == 0 && !runner->_running) delete runner;}
|
||||||
|
|
||||||
|
/// Run CURLM requests and completion handlers, as well as other periodic jobs.
|
||||||
|
class Runner {
|
||||||
|
G_DEFINE_EXCEPTION (RunnerEx);
|
||||||
|
/// Free CURL during stack unwinding.
|
||||||
|
struct FreeCurl {
|
||||||
|
Runner* runner; CURL* curl;
|
||||||
|
FreeCurl (Runner* runner, CURL* curl): runner (runner), curl (curl) {}
|
||||||
|
~FreeCurl() {
|
||||||
|
runner->_handlers.erase (curl);
|
||||||
|
curl_multi_remove_handle (runner->_curlm, curl);
|
||||||
|
curl_easy_cleanup (curl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
public:
|
||||||
|
struct JobInfo;
|
||||||
|
/// The job must return `true` if Runner is to continue invoking it.
|
||||||
|
typedef std::function<bool(JobInfo& jobInfo)> job_t;
|
||||||
|
struct JobInfo {
|
||||||
|
job_t job;
|
||||||
|
float pauseSec = 1.0f;
|
||||||
|
struct timespec ran = {0, 0};
|
||||||
|
};
|
||||||
|
protected:
|
||||||
|
typedef std::function<void(CURLMsg*)> handler_t;
|
||||||
|
typedef std::function<void(const char* error)> errlog_t;
|
||||||
|
std::shared_ptr<struct event_base> _evbase;
|
||||||
|
errlog_t _errlog;
|
||||||
|
std::recursive_mutex _mutex;
|
||||||
|
typedef std::unique_ptr<struct event, void(*)(struct event*)> event_t;
|
||||||
|
std::unordered_map<CURL*, std::pair<handler_t, event_t>> _handlers;
|
||||||
|
/// Functions to run periodically.
|
||||||
|
typedef std::unordered_map<gstring, JobInfo> jobs_map_t;
|
||||||
|
jobs_map_t _jobs;
|
||||||
|
CURLM* _curlm = nullptr;
|
||||||
|
struct event* _timer = nullptr;
|
||||||
|
|
||||||
|
/// Schedule a function to be run on the event loop. Useful to run all cURL methods on the single event loop thread.
|
||||||
|
template<typename F>
|
||||||
|
void doInEv (F fun, struct timeval after = {0, 0}) {
|
||||||
|
struct Dugout {F fun; struct event* timer; Dugout (F&& fun): fun (std::move (fun)), timer (nullptr) {}} *dugout = new Dugout (std::move (fun));
|
||||||
|
event_callback_fn cb = [](evutil_socket_t, short, void* dugout_)->void {
|
||||||
|
Dugout* dugout = static_cast<Dugout*> (dugout_);
|
||||||
|
event_free (dugout->timer); dugout->timer = nullptr;
|
||||||
|
F fun = std::move (dugout->fun); delete dugout;
|
||||||
|
fun();
|
||||||
|
};
|
||||||
|
dugout->timer = evtimer_new (_evbase.get(), cb, dugout);
|
||||||
|
evtimer_add (dugout->timer, &after);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldRun (jobs_map_t::value_type& entry, const struct timespec& ct) {
|
||||||
|
JobInfo& jobInfo = entry.second;
|
||||||
|
if (jobInfo.pauseSec <= 0.f) return true; // Run always.
|
||||||
|
if (jobInfo.ran.tv_sec == 0) {jobInfo.ran = ct; return true;}
|
||||||
|
float delta = (float)(ct.tv_sec - jobInfo.ran.tv_sec);
|
||||||
|
delta += (float)(ct.tv_nsec - jobInfo.ran.tv_nsec) / 1000000000.0f;
|
||||||
|
if (delta >= jobInfo.pauseSec) {jobInfo.ran = ct; return true;}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for debugging.
|
||||||
|
static uint64_t ms() {
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds> (std::chrono::system_clock::now().time_since_epoch()) .count();
|
||||||
|
}
|
||||||
|
/// Tells CURL to check its sockets.
|
||||||
|
void callCurlWithTimeout() {
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": callCurlWithTimeout" << std::endl;
|
||||||
|
int running_handles = 0;
|
||||||
|
CURLMcode rc = curl_multi_socket_action (_curlm, CURL_SOCKET_TIMEOUT, 0, &running_handles);
|
||||||
|
if (rc != CURLM_OK) {GSTRING_ON_STACK (err, 256) << "glim::Runner: curl_multi_socket_action: " << curl_multi_strerror (rc); _errlog (err.c_str());}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should only be run when the _mutex is locked.
|
||||||
|
void checkForFinishedCurlJobs() {
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": checkForFinishedCurlJobs" << std::endl;
|
||||||
|
nextMessage:
|
||||||
|
int msgs_in_queue = 0;
|
||||||
|
CURLMsg* msg = curl_multi_info_read (_curlm, &msgs_in_queue);
|
||||||
|
if (msg) try {
|
||||||
|
auto curl = msg->easy_handle;
|
||||||
|
FreeCurl freeCurl (this, curl);
|
||||||
|
auto it = _handlers.find (curl);
|
||||||
|
if (it != _handlers.end()) it->second.first (msg);
|
||||||
|
if (msgs_in_queue > 0) goto nextMessage;
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
char eBuf[512]; gstring err (sizeof(eBuf), eBuf, false, 0);
|
||||||
|
err << "glim::Runner: handler: " << ex.what();
|
||||||
|
_errlog (err.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Will reset the timer unless there is a shorter timer already set.
|
||||||
|
void restartTimer (uint32_t nextInMicro = 100000) { // 100ms = 100000µs
|
||||||
|
struct timeval tv;
|
||||||
|
if (event_pending (_timer, EV_TIMEOUT, &tv) && !tv.tv_sec && tv.tv_usec < nextInMicro) return; // Already have a shorter timeout.
|
||||||
|
tv = {0, nextInMicro};
|
||||||
|
evtimer_add (_timer, &tv);
|
||||||
|
}
|
||||||
|
static void evTimerCB (evutil_socket_t, short, void* runner_) {
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": evTimerCB" << std::endl;
|
||||||
|
Runner* runner = (Runner*) runner_;
|
||||||
|
runner->callCurlWithTimeout();
|
||||||
|
runner->run();
|
||||||
|
}
|
||||||
|
/// event_callback_fn: There is an activity on a socket we are monitoring for CURL.
|
||||||
|
static void evSocketCB (evutil_socket_t sock, short events, void* runner_) {
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": evSocketCB; sock: " << sock << "; events: " << events << std::endl;
|
||||||
|
Runner* runner = (Runner*) runner_;
|
||||||
|
int ev_bitmask = (events & EV_READ ? CURL_CSELECT_IN : 0) | (events & EV_WRITE ? CURL_CSELECT_OUT : 0);
|
||||||
|
int running_handles = 0;
|
||||||
|
CURLMcode rc = curl_multi_socket_action (runner->_curlm, sock, ev_bitmask, &running_handles);
|
||||||
|
if (rc != CURLM_OK) {GSTRING_ON_STACK (err, 256) << "glim::Runner: curl_multi_socket_action: " << curl_multi_strerror (rc); runner->_errlog (err.c_str());}
|
||||||
|
}
|
||||||
|
static void deleteEvent (struct event* ev) {
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": deleteEvent: " << ev << std::endl;
|
||||||
|
event_del (ev); event_free (ev);
|
||||||
|
};
|
||||||
|
/// curl_socket_callback: CURL asks us to monitor the socket.
|
||||||
|
static int curlSocketCB (CURL* easy, curl_socket_t sock, int what, void* runner_, void* socketp) {
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": curlSocketCB; sock: " << sock << "; what: " << what;
|
||||||
|
//std::cout << " (" << (what == 0 ? "none" : what == 1 ? "in" : what == 2 ? "out" : what == 3 ? "inout" : what == 4 ? "remove" : "?") << ")" << std::endl;
|
||||||
|
Runner* runner = (Runner*) runner_;
|
||||||
|
std::lock_guard<std::recursive_mutex> lock (runner->_mutex);
|
||||||
|
if (what & CURL_POLL_REMOVE) {
|
||||||
|
auto it = runner->_handlers.find (easy); if (it != runner->_handlers.end()) it->second.second.reset();
|
||||||
|
// We can't run `checkForFinishedCurlJobs` from there or bad things would happen
|
||||||
|
// (`curl_multi_remove_handle` will be called while we are still in the `curl_multi_socket_action`),
|
||||||
|
// but we can schedule the check via the libevent timer.
|
||||||
|
runner->restartTimer (0);
|
||||||
|
} else {
|
||||||
|
auto it = runner->_handlers.find (easy); if (it != runner->_handlers.end() && !it->second.second) {
|
||||||
|
event_callback_fn cb = evSocketCB;
|
||||||
|
struct event* ev = event_new (runner->_evbase.get(), sock, EV_READ | EV_WRITE | EV_ET | EV_PERSIST, cb, runner);
|
||||||
|
event_add (ev, nullptr);
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": new event: " << ev << std::endl;
|
||||||
|
it->second.second = event_t (ev, deleteEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// curl_multi_timer_callback: Schedule a CURL timer event or if `timeout_ms` is 0 then run immediately.
|
||||||
|
static int curlTimerCB (CURLM* multi, long timeout_ms, void* runner_) {
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": curlTimerCB; timeout_ms: " << timeout_ms << std::endl;
|
||||||
|
if (timeout_ms == -1) return 0; // CURL tells us it doesn't need no timer.
|
||||||
|
Runner* runner = (Runner*) runner_;
|
||||||
|
if (timeout_ms == 0) { // CURL tells us it wants to run NOW.
|
||||||
|
runner->callCurlWithTimeout();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// CURL asks us to run it `timeout_ms` from now.
|
||||||
|
runner->restartTimer (std::min ((uint32_t) timeout_ms, (uint32_t) 100) * 1000); // We wait no more than 100ms.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
Runner (std::shared_ptr<struct event_base> evbase, errlog_t errlog): _evbase (evbase), _errlog (errlog) {
|
||||||
|
doInEv ([this]() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock (_mutex);
|
||||||
|
_curlm = curl_multi_init(); if (!_curlm) GNTHROW (RunnerEx, "!curl_multi_init");
|
||||||
|
auto check = [this](CURLMcode rc) {if (rc != CURLM_OK) {curl_multi_cleanup (_curlm); GNTHROW (RunnerEx, "curl_multi_setopt: " + std::to_string (rc));}};
|
||||||
|
check (curl_multi_setopt (_curlm, CURLMOPT_SOCKETDATA, this));
|
||||||
|
curl_socket_callback socketCB = curlSocketCB; check (curl_multi_setopt (_curlm, CURLMOPT_SOCKETFUNCTION, socketCB));
|
||||||
|
check (curl_multi_setopt (_curlm, CURLMOPT_TIMERDATA, this));
|
||||||
|
curl_multi_timer_callback curlTimerCB_ = curlTimerCB; check (curl_multi_setopt (_curlm, CURLMOPT_TIMERFUNCTION, curlTimerCB_));
|
||||||
|
event_callback_fn evTimerCB_ = evTimerCB; _timer = evtimer_new (_evbase.get(), evTimerCB_, this);
|
||||||
|
restartTimer();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
~Runner() {
|
||||||
|
//std::cout << __LINE__ << ',' << ms() << ": ~Runner" << std::endl;
|
||||||
|
std::lock_guard<std::recursive_mutex> lock (_mutex);
|
||||||
|
if (_timer) {evtimer_del (_timer); event_free (_timer); _timer = nullptr;}
|
||||||
|
doInEv ([curlm = _curlm, handlers = std::move (_handlers)]() {
|
||||||
|
for (auto it = handlers.begin(), end = handlers.end(); it != end; ++it) {
|
||||||
|
curl_multi_remove_handle (curlm, it->first);
|
||||||
|
curl_easy_cleanup (it->first);
|
||||||
|
}
|
||||||
|
if (curlm) {curl_multi_cleanup (curlm);}
|
||||||
|
});
|
||||||
|
_curlm = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Turns HTTP Pipelining on (or off).
|
||||||
|
* See http://curl.haxx.se/libcurl/c/curl_multi_setopt.html#CURLMOPTPIPELINING */
|
||||||
|
Runner& pipeline (long enabled = 1) {
|
||||||
|
CURLMcode rc = curl_multi_setopt (_curlm, CURLMOPT_PIPELINING, enabled);
|
||||||
|
if (rc != CURLM_OK) GNTHROW (RunnerEx, "curl_multi_setopt: " + std::to_string (rc));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for the operation to complete, then call the `handler`, then free the `curl`.
|
||||||
|
void multi (CURL* curl, handler_t handler) {
|
||||||
|
{ std::lock_guard<std::recursive_mutex> lock (_mutex);
|
||||||
|
_handlers.insert (std::make_pair (curl, std::make_pair (std::move (handler), event_t (nullptr, nullptr)))); }
|
||||||
|
doInEv ([this,curl]() {
|
||||||
|
curl_multi_add_handle (_curlm, curl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/// Register a new job to be run on the thread loop.
|
||||||
|
JobInfo& job (const gstring& name) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock (_mutex);
|
||||||
|
return _jobs[name];
|
||||||
|
}
|
||||||
|
/// Register a new job to be run on the thread loop.
|
||||||
|
void schedule (const gstring& name, float pauseSec, job_t job) {
|
||||||
|
struct timespec ct; if (pauseSec > 0.f) clock_gettime (CLOCK_MONOTONIC, &ct);
|
||||||
|
std::lock_guard<std::recursive_mutex> lock (_mutex);
|
||||||
|
JobInfo& jobInfo = _jobs[name];
|
||||||
|
jobInfo.job = job;
|
||||||
|
jobInfo.pauseSec = pauseSec;
|
||||||
|
if (pauseSec > 0.f) jobInfo.ran = ct; // If we need a pause then we also need to know when the job was scheduled.
|
||||||
|
}
|
||||||
|
/// Register a new job to be run on the thread loop.
|
||||||
|
void schedule (float pauseSec, job_t job) {
|
||||||
|
// Find a unique job name.
|
||||||
|
anotherName:
|
||||||
|
GSTRING_ON_STACK (name, 64) << "job" << rand();
|
||||||
|
if (_jobs.find (name) != _jobs.end()) goto anotherName;
|
||||||
|
schedule (name, pauseSec, std::move (job));
|
||||||
|
}
|
||||||
|
void removeJob (const gstring& name) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock (_mutex);
|
||||||
|
_jobs.erase (name);
|
||||||
|
}
|
||||||
|
/// Invoked automatically from a libevent timer; can also be invoked manually.
|
||||||
|
void run() {
|
||||||
|
_mutex.lock();
|
||||||
|
checkForFinishedCurlJobs();
|
||||||
|
// Run non-CURL jobs. Copy jobs into a local array in order not to run them with the `_mutex` locked.
|
||||||
|
struct timespec ct; clock_gettime (CLOCK_MONOTONIC, &ct);
|
||||||
|
JobInfo jobs[_jobs.size()]; gstring jobNames[_jobs.size()]; int jn = -1; {
|
||||||
|
for (auto it = _jobs.begin(), end = _jobs.end(); it != end; ++it) if (shouldRun (*it, ct)) {
|
||||||
|
++jn; jobNames[jn] = it->first; jobs[jn] = it->second;
|
||||||
|
} }
|
||||||
|
_mutex.unlock();
|
||||||
|
|
||||||
|
for (; jn >= 0; --jn) try {
|
||||||
|
if (!jobs[jn].job (jobs[jn])) removeJob (jobNames[jn]);
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
char eBuf[512]; gstring err (sizeof(eBuf), eBuf, false, 0);
|
||||||
|
err << "glim::Runner: error in job " << jobNames[jn] << ": " << ex.what();
|
||||||
|
_errlog (err.c_str());
|
||||||
|
}
|
||||||
|
restartTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expose CURLM. Useful for curl_multi_setopt (http://curl.haxx.se/libcurl/c/curl_multi_setopt.html).
|
||||||
|
CURLM* curlm() const {return _curlm;}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace glim
|
||||||
|
|
||||||
|
#endif // _GLIM_RUNNER_INCLUDED
|
|
@ -0,0 +1,538 @@
|
||||||
|
#ifndef GLIM_SQLITE_HPP_
|
||||||
|
#define GLIM_SQLITE_HPP_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A threaded interface to <a href="http://sqlite.org/">SQLite</a>.
|
||||||
|
* This file is a header-only library,
|
||||||
|
* whose sole dependencies should be standard STL and posix threading libraries.
|
||||||
|
* You can extract this file out of the "glim" library to include it separately in your project.
|
||||||
|
* @code
|
||||||
|
Copyright 2006-2012 Kozarezov Artem Aleksandrovich
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
* @endcode
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <string.h> // strerror
|
||||||
|
#include <sys/types.h> // stat
|
||||||
|
#include <sys/stat.h> // stat
|
||||||
|
#include <unistd.h> // stat
|
||||||
|
#include <errno.h> // stat
|
||||||
|
#include <stdio.h> // snprintf
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace glim {
|
||||||
|
|
||||||
|
class SqliteSession;
|
||||||
|
class SqliteQuery;
|
||||||
|
|
||||||
|
struct SqliteEx: public std::runtime_error {
|
||||||
|
SqliteEx (const std::string& what): std::runtime_error (what) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The database.
|
||||||
|
* According to sqlite3_open <a href="http://sqlite.org/capi3ref.html#sqlite3_open">documentation</a>,
|
||||||
|
* only the thread that opened the database can safely access it. This restriction was
|
||||||
|
* relaxed, as described in the <a href="http://www.sqlite.org/faq.html#q8">FAQ</a>
|
||||||
|
* (the "Is SQLite threadsafe?" question), so we can use the library from multiple
|
||||||
|
* threads, but only if no more than one thread at a time accesses the database.
|
||||||
|
* This restriction is, in fact, beneficial if the database is used from a single application:
|
||||||
|
* by restricting access to a sigle thread at a time, we effectively avoid all deadlock issues.\n
|
||||||
|
* This library goals are:\n
|
||||||
|
* \li to ensure that SQLite is used in a thread-safe way,
|
||||||
|
* \li to provide additional threaded quirks, such as delayed updates (not implemented).
|
||||||
|
*
|
||||||
|
* The library is targeted at SQLite setup which is \b not \c -DTHREADSAFE,
|
||||||
|
* since this is the default setup on UNIX architectures.\n
|
||||||
|
* \n
|
||||||
|
* This file is a header-only library,
|
||||||
|
* whose sole dependencies should be standard STL and posix threading libraries.
|
||||||
|
* You can extract this file out of the "glim" library to include it separately in your project.\n
|
||||||
|
* \n
|
||||||
|
* This library is targeted at UTF-8 API. There is no plans to support the UTF-16 API.\n
|
||||||
|
* \n
|
||||||
|
* See also:\n
|
||||||
|
* \li http://www.sqlite.org/cvstrac/fileview?f=sqlite/src/server.c\n
|
||||||
|
* for another way of handling multithreading with SQLite.
|
||||||
|
*/
|
||||||
|
class Sqlite {
|
||||||
|
/// No copying allowed.
|
||||||
|
Sqlite& operator = (const Sqlite& other) {return *this;}
|
||||||
|
/// No copying allowed.
|
||||||
|
Sqlite (const Sqlite& other) = delete;
|
||||||
|
friend class SqliteSession;
|
||||||
|
protected:
|
||||||
|
/// Filename the database was opened with; we need it to reopen the database on fork()s.
|
||||||
|
/// std::string is used to avoid memory allocation issues.
|
||||||
|
std::string filename;
|
||||||
|
::sqlite3* handler;
|
||||||
|
::pthread_mutex_t mutex;
|
||||||
|
public:
|
||||||
|
/// Flags for the Sqlite constructor.
|
||||||
|
enum Flags {
|
||||||
|
/**
|
||||||
|
* The file will be checked for existence.
|
||||||
|
* SqliteEx is thrown if the file is not accessible;
|
||||||
|
* format of the error description is "$filename: $strerror".\n
|
||||||
|
* Usage example: \code Sqlite db ("filename", Sqlite::existing); \endcode
|
||||||
|
*/
|
||||||
|
existing = 1
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Opens the database.
|
||||||
|
* @param filename Database filename (UTF-8).
|
||||||
|
* @param flags Optional. Currently there is the #existing flag.
|
||||||
|
* @throws SqliteEx Thrown if we can't open the database.
|
||||||
|
*/
|
||||||
|
Sqlite (std::string filename, int flags = 0) {
|
||||||
|
if (flags & existing) {
|
||||||
|
// Check if the file exists already.
|
||||||
|
struct stat st; if (stat (filename.c_str(), &st))
|
||||||
|
throw SqliteEx (filename + ": " + ::strerror(errno));
|
||||||
|
}
|
||||||
|
::pthread_mutex_init (&mutex, NULL);
|
||||||
|
this->filename = filename;
|
||||||
|
if (::sqlite3_open(filename.c_str(), &handler) != SQLITE_OK)
|
||||||
|
throw SqliteEx (std::string("sqlite3_open(") + filename + "): " + ::sqlite3_errmsg(handler));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Closes the database.
|
||||||
|
* @throws SqliteEx Thrown if we can't close the database.
|
||||||
|
*/
|
||||||
|
~Sqlite () {
|
||||||
|
::pthread_mutex_destroy (&mutex);
|
||||||
|
if (::sqlite3_close(handler) != SQLITE_OK)
|
||||||
|
throw SqliteEx (std::string ("sqlite3_close(): ") + ::sqlite3_errmsg(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
Sqlite& exec (const char* query);
|
||||||
|
/**
|
||||||
|
* Invokes `exec` on `query.c_str()`.
|
||||||
|
* Example:\code
|
||||||
|
* glim::Sqlite sqlite (":memory:");
|
||||||
|
* for (std::string pv: {"page_size = 4096", "secure_delete = 1"}) sqlite->exec2 ("PRAGMA " + pv); \endcode
|
||||||
|
*/
|
||||||
|
template <typename StringLike> Sqlite& exec2 (StringLike query) {return exec (query.c_str());}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single thread session with Sqlite.
|
||||||
|
* Only a sigle thread at a time can have an SqliteSession,
|
||||||
|
* all other threads will wait, in the SqliteSession constructor,
|
||||||
|
* till the active session is either closed or destructed.
|
||||||
|
*/
|
||||||
|
class SqliteSession {
|
||||||
|
/// No copying allowed.
|
||||||
|
SqliteSession& operator = (const SqliteSession& other) {return *this;}
|
||||||
|
/// No copying allowed.
|
||||||
|
SqliteSession(SqliteSession& other): db (NULL) {}
|
||||||
|
protected:
|
||||||
|
Sqlite* db;
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Locks the database.
|
||||||
|
* @throws SqliteEx if a mutex error occurs.
|
||||||
|
*/
|
||||||
|
SqliteSession (Sqlite* sqlite): db (sqlite) {
|
||||||
|
int err = ::pthread_mutex_lock (&(db->mutex));
|
||||||
|
if (err != 0) throw SqliteEx (std::string ("error locking the mutex: ") + ::strerror(err));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* A shorter way to construct query from the session.
|
||||||
|
* Usage example: \code ses.query(S("create table test (i integer)")).step() \endcode
|
||||||
|
* @see SqliteQuery#qstep
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
SqliteQuery query (T t);
|
||||||
|
/// Automatically unlocks the database.
|
||||||
|
/// @see close
|
||||||
|
~SqliteSession () {close();}
|
||||||
|
/**
|
||||||
|
* Unlock the database.
|
||||||
|
* It is safe to call this method multiple times.\n
|
||||||
|
* You must not use the session after it was closed.\n
|
||||||
|
* All resources allocated within this session must be released before the session is closed.
|
||||||
|
* @throws SqliteEx if a mutex error occurs.
|
||||||
|
*/
|
||||||
|
void close () {
|
||||||
|
if (db == NULL) return;
|
||||||
|
int err = ::pthread_mutex_unlock (&(db->mutex));
|
||||||
|
db = NULL;
|
||||||
|
if (err != 0) throw SqliteEx (std::string ("error unlocking the mutex: ") + ::strerror(err));
|
||||||
|
}
|
||||||
|
/// True if the \c close method has been already called on this SqliteSession.
|
||||||
|
bool isClosed () const {
|
||||||
|
return db == NULL;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This class can be used in place of the SQLite handler.
|
||||||
|
* Make sure you've released any resources thus manually acquired before this SqliteSession is closed.
|
||||||
|
* Usage example:
|
||||||
|
* @code
|
||||||
|
* glim::Sqlite db (":memory:");
|
||||||
|
* glim::SqliteSession ses (&db);
|
||||||
|
* sqlite3_exec (ses, "PRAGMA page_size = 4096;", NULL, NULL, NULL);
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
operator ::sqlite3* () const {return db->handler;}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the given query, throwing SqliteEx on failure.\n
|
||||||
|
* Example:\code
|
||||||
|
* glim::Sqlite sqlite (":memory:");
|
||||||
|
* sqlite.exec ("PRAGMA page_size = 4096") .exec ("PRAGMA secure_delete = 1"); \endcode
|
||||||
|
*/
|
||||||
|
inline Sqlite& Sqlite::exec (const char* query) {
|
||||||
|
SqliteSession ses (this); // Maintains the locks.
|
||||||
|
char* errmsg = NULL; ::sqlite3_exec (handler, query, NULL, NULL, &errmsg);
|
||||||
|
if (errmsg) throw SqliteEx (std::string ("Sqlite::exec, error in query (") + query + "): " + errmsg);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the sqlite3_stmt; will prepare it, bind values, query and finalize.
|
||||||
|
*/
|
||||||
|
class SqliteQuery {
|
||||||
|
protected:
|
||||||
|
::sqlite3_stmt* statement;
|
||||||
|
SqliteSession* session;
|
||||||
|
int bindCounter;
|
||||||
|
/// -1 if statement isn't DONE.
|
||||||
|
int mChanges;
|
||||||
|
void prepare (SqliteSession* session, char const* query, int queryLength) {
|
||||||
|
::sqlite3* handler = *session;
|
||||||
|
if (::sqlite3_prepare_v2 (handler, query, queryLength, &statement, NULL) != SQLITE_OK)
|
||||||
|
throw SqliteEx (std::string(query, queryLength) + ": " + ::sqlite3_errmsg(handler));
|
||||||
|
}
|
||||||
|
/** Shan't copy. */
|
||||||
|
SqliteQuery (const SqliteQuery& other) = delete;
|
||||||
|
public:
|
||||||
|
SqliteQuery (SqliteQuery&& rvalue) {
|
||||||
|
statement = rvalue.statement;
|
||||||
|
session = rvalue.session;
|
||||||
|
bindCounter = rvalue.bindCounter;
|
||||||
|
mChanges = rvalue.mChanges;
|
||||||
|
rvalue.statement = nullptr;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Prepares the query.
|
||||||
|
* @throws SqliteEx if sqlite3_prepare fails; format of the error message is "$query: $errmsg".
|
||||||
|
*/
|
||||||
|
SqliteQuery (SqliteSession* session, char const* query, int queryLength)
|
||||||
|
: statement (NULL), session (session), bindCounter (0), mChanges (-1) {
|
||||||
|
prepare (session, query, queryLength);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Prepares the query.
|
||||||
|
* @throws SqliteEx if sqlite3_prepare fails; format of the error message is "$query: $errmsg".
|
||||||
|
*/
|
||||||
|
SqliteQuery (SqliteSession* session, std::pair<char const*, int> query)
|
||||||
|
: statement (NULL), session (session), bindCounter (0), mChanges (-1) {
|
||||||
|
prepare (session, query.first, query.second);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Prepares the query.
|
||||||
|
* @throws SqliteEx if sqlite3_prepare fails; format of the error message is "$query: $errmsg".
|
||||||
|
*/
|
||||||
|
SqliteQuery (SqliteSession* session, std::string query)
|
||||||
|
: statement (NULL), session (session), bindCounter (0), mChanges (-1) {
|
||||||
|
prepare (session, query.c_str(), query.length());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Release resources.
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_finalize
|
||||||
|
*/
|
||||||
|
~SqliteQuery () {
|
||||||
|
if (statement) ::sqlite3_finalize (statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call this (followed by the #step) if you need the query to be re-executed.
|
||||||
|
/// @see http://sqlite.org/capi3ref.html#sqlite3_reset
|
||||||
|
SqliteQuery& reset () {
|
||||||
|
bindCounter = 0;
|
||||||
|
mChanges = -1;
|
||||||
|
::sqlite3_reset (statement);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synonym for #step.
|
||||||
|
bool next () {return step();}
|
||||||
|
/**
|
||||||
|
* Invokes sqlite3_step.
|
||||||
|
* @return \c true if there was a row fetched successfully, \c false if there is no more rows.
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_step
|
||||||
|
*/
|
||||||
|
bool step () {
|
||||||
|
if (mChanges >= 0) {mChanges = 0; return false;}
|
||||||
|
int ret = ::sqlite3_step (statement);
|
||||||
|
if (ret == SQLITE_ROW) return true;
|
||||||
|
if (ret == SQLITE_DONE) {
|
||||||
|
mChanges = ::sqlite3_changes (*session);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw SqliteEx (std::string(::sqlite3_errmsg(*session)));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Perform #step and throw an exception if #step has returned \c false.
|
||||||
|
* Usage example:
|
||||||
|
* \code (ses.query(S("select count(*) from test where idx = ?")) << 12345).qstep().intAt(1) \endcode
|
||||||
|
*/
|
||||||
|
SqliteQuery& qstep () {
|
||||||
|
if (!step())
|
||||||
|
throw SqliteEx (std::string("qstep: no rows returned / affected"));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Invokes a DML query and returns the number of rows affected.
|
||||||
|
* Example: \code
|
||||||
|
* int affected = (ses.query(S("update test set count = count + ? where id = ?")) << 1 << 9).ustep();
|
||||||
|
* \endcode
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_step
|
||||||
|
*/
|
||||||
|
int ustep () {
|
||||||
|
int ret = ::sqlite3_step (statement);
|
||||||
|
if (ret == SQLITE_DONE) {
|
||||||
|
mChanges = ::sqlite3_changes (*session);
|
||||||
|
return mChanges;
|
||||||
|
}
|
||||||
|
if (ret == SQLITE_ROW) return 0;
|
||||||
|
throw SqliteEx (std::string(::sqlite3_errmsg(*session)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of rows changed by the query.
|
||||||
|
* Providing the query was a DML (Data Modification Language),
|
||||||
|
* returns the number of rows updated.\n
|
||||||
|
* If the query wasn't a DML, returned value is undefined.\n
|
||||||
|
* -1 is returned if the query wasn't executed, or after #reset.\n
|
||||||
|
* Example: \code
|
||||||
|
* SqliteQuery query (&ses, S("update test set count = count + ? where id = ?"));
|
||||||
|
* query.bind (1, 1);
|
||||||
|
* query.bind (2, 9);
|
||||||
|
* query.step ();
|
||||||
|
* int affected = query.changes ();
|
||||||
|
* \endcode
|
||||||
|
* @see #ustep
|
||||||
|
*/
|
||||||
|
int changes () {return mChanges;}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The integer value of the given column.
|
||||||
|
* @param column 1-based.
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_column_text
|
||||||
|
*/
|
||||||
|
int intAt (int column) {
|
||||||
|
return ::sqlite3_column_int (statement, --column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The integer value of the given column.
|
||||||
|
* @param column 1-based.
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_column_text
|
||||||
|
*/
|
||||||
|
sqlite3_int64 int64at (int column) {
|
||||||
|
return ::sqlite3_column_int64 (statement, --column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The floating point number from the given column.
|
||||||
|
* @param column 1-based.
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_column_text
|
||||||
|
*/
|
||||||
|
double doubleAt (int column) {
|
||||||
|
return ::sqlite3_column_double (statement, --column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the column as UTF-8 characters, which can be used until the next #step.
|
||||||
|
* @param column 1-based.
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_column_text
|
||||||
|
*/
|
||||||
|
std::pair<char const*, int> charsAt (int column) {
|
||||||
|
return std::pair<char const*, int> ((char const*) ::sqlite3_column_text (statement, column-1),
|
||||||
|
::sqlite3_column_bytes (statement, column-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the column as C++ string (UTF-8).
|
||||||
|
* @param column 1-based.
|
||||||
|
*/
|
||||||
|
std::string stringAt (int column) {
|
||||||
|
return std::string ((char const*) ::sqlite3_column_text (statement, column-1),
|
||||||
|
::sqlite3_column_bytes (statement, column-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the column.
|
||||||
|
* SQLITE_INTEGER, SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB or SQLITE_NULL.
|
||||||
|
* @param column 1-based.
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_column_text
|
||||||
|
*/
|
||||||
|
int typeAt (int column) {
|
||||||
|
return ::sqlite3_column_type (statement, --column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a value using one of the bind methods.
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
SqliteQuery& operator << (T value) {
|
||||||
|
return bind (++bindCounter, value);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Binds a value using the named parameter and one of the bind methods.
|
||||||
|
* @throws SqliteEx if the name could not be found.
|
||||||
|
* @see http://sqlite.org/capi3ref.html#sqlite3_bind_parameter_index
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
SqliteQuery& bind (char const* name, T value) {
|
||||||
|
int index = ::sqlite3_bind_parameter_index (statement, name);
|
||||||
|
if (index == 0)
|
||||||
|
throw SqliteEx (std::string ("No such parameter in the query: ") + name);
|
||||||
|
return bind (index, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a string to the query.
|
||||||
|
* @param transient must be true, if lifetime of the string might be shorter than that of the query.
|
||||||
|
*/
|
||||||
|
SqliteQuery& bind (int index, const char* text, int length, bool transient = false) {
|
||||||
|
if (::sqlite3_bind_text (statement, index, text, length,
|
||||||
|
transient ? SQLITE_TRANSIENT : SQLITE_STATIC) != SQLITE_OK)
|
||||||
|
throw SqliteEx (std::string (::sqlite3_errmsg (*session)));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Bind a string to the query.
|
||||||
|
* @param transient must be true, if lifetime of the string might be shorter than that of the query.
|
||||||
|
*/
|
||||||
|
SqliteQuery& bind (int index, std::pair<const char*, int> text, bool transient = false) {
|
||||||
|
if (::sqlite3_bind_text (statement, index, text.first, text.second,
|
||||||
|
transient ? SQLITE_TRANSIENT : SQLITE_STATIC) != SQLITE_OK)
|
||||||
|
throw SqliteEx (std::string (::sqlite3_errmsg (*session)));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Bind a string to the query.
|
||||||
|
* @param transient must be true, if lifetime of the string might be shorter than that of the query.
|
||||||
|
*/
|
||||||
|
SqliteQuery& bind (int index, const std::string& text, bool transient = true) {
|
||||||
|
if (::sqlite3_bind_text (statement, index, text.data(), text.length(),
|
||||||
|
transient ? SQLITE_TRANSIENT : SQLITE_STATIC) != SQLITE_OK)
|
||||||
|
throw SqliteEx (std::string (::sqlite3_errmsg (*session)));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Bind an integer to the query.
|
||||||
|
*/
|
||||||
|
SqliteQuery& bind (int index, int value) {
|
||||||
|
if (::sqlite3_bind_int (statement, index, value) != SQLITE_OK)
|
||||||
|
throw SqliteEx (std::string (::sqlite3_errmsg (*session)));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Bind an 64-bit integer to the query.
|
||||||
|
*/
|
||||||
|
SqliteQuery& bind (int index, sqlite3_int64 value) {
|
||||||
|
if (::sqlite3_bind_int64 (statement, index, value) != SQLITE_OK)
|
||||||
|
throw SqliteEx (std::string (::sqlite3_errmsg (*session)));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version of SqliteQuery suitable for using SQLite in parallel with other processes.
|
||||||
|
* Will automatically handle the SQLITE_SCHEMA error
|
||||||
|
* and will automatically repeat attempts after SQLITE_BUSY,
|
||||||
|
* but it requires that the query string supplied
|
||||||
|
* is constant and available during the SqliteParQuery lifetime.
|
||||||
|
* Error messages, contained in exceptions, may differ from SqliteQuery by containing the query
|
||||||
|
* (for example, the #step method will throw "$query: $errmsg" instead of just "$errmsg").
|
||||||
|
*/
|
||||||
|
class SqliteParQuery: public SqliteQuery {
|
||||||
|
protected:
|
||||||
|
char const* query;
|
||||||
|
int queryLength;
|
||||||
|
int repeat;
|
||||||
|
int wait;
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Prepares the query.
|
||||||
|
* @param repeat the number of times we try to repeat the query when SQLITE_BUSY is returned.
|
||||||
|
* @param wait how long, in milliseconds (1/1000 of a second) we are to wait before repeating.
|
||||||
|
* @throws SqliteEx if sqlite3_prepare fails; format of the error message is "$query: $errmsg".
|
||||||
|
*/
|
||||||
|
SqliteParQuery (SqliteSession* session, char const* query, int queryLength, int repeat = 90, int wait = 20)
|
||||||
|
: SqliteQuery (session, query, queryLength) {
|
||||||
|
this->query = query;
|
||||||
|
this->queryLength = queryLength;
|
||||||
|
this->repeat = repeat;
|
||||||
|
this->wait = wait;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Prepares the query.
|
||||||
|
* @param query the SQL query together with its length.
|
||||||
|
* @param repeat the number of times we try to repeat the query when SQLITE_BUSY is returned.
|
||||||
|
* @param wait how long, in milliseconds (1/1000 of a second) we are to wait before repeating.
|
||||||
|
* @throws SqliteEx if sqlite3_prepare fails; format of the error message is "$query: $errmsg".
|
||||||
|
*/
|
||||||
|
SqliteParQuery (SqliteSession* session, std::pair<char const*, int> query, int repeat = 90, int wait = 20)
|
||||||
|
: SqliteQuery (session, query) {
|
||||||
|
this->query = query.first;
|
||||||
|
this->queryLength = query.second;
|
||||||
|
this->repeat = repeat;
|
||||||
|
this->wait = wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool next () {return step();}
|
||||||
|
bool step () {
|
||||||
|
if (mChanges >= 0) {mChanges = 0; return false;}
|
||||||
|
repeat:
|
||||||
|
int ret = ::sqlite3_step (statement);
|
||||||
|
if (ret == SQLITE_ROW) return true;
|
||||||
|
if (ret == SQLITE_DONE) {
|
||||||
|
mChanges = ::sqlite3_changes (*session);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ret == SQLITE_SCHEMA) {
|
||||||
|
::sqlite3_stmt* old = statement;
|
||||||
|
prepare (session, query, queryLength);
|
||||||
|
::sqlite3_transfer_bindings(old, statement);
|
||||||
|
::sqlite3_finalize (old);
|
||||||
|
goto repeat;
|
||||||
|
}
|
||||||
|
if (ret == SQLITE_BUSY) for (int repeat = this->repeat; ret == SQLITE_BUSY && repeat >= 0; --repeat) {
|
||||||
|
//struct timespec ts; ts.tv_sec = 0; ts.tv_nsec = wait * 1000000; // nan is 10^-9 of sec.
|
||||||
|
//while (::nanosleep (&ts, &ts) == EINTR);
|
||||||
|
::sqlite3_sleep (wait);
|
||||||
|
ret = ::sqlite3_step (statement);
|
||||||
|
}
|
||||||
|
throw SqliteEx (std::string(query, queryLength) + ::sqlite3_errmsg(*session));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
SqliteQuery SqliteSession::query (T t) {
|
||||||
|
return SqliteQuery (this, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
}; // namespace glim
|
||||||
|
|
||||||
|
#endif // GLIM_SQLITE_HPP_
|
|
@ -0,0 +1,89 @@
|
||||||
|
// http://en.wikipedia.org/wiki/Setcontext; man 3 makecontext; man 2 getcontext
|
||||||
|
// http://www.boost.org/doc/libs/1_53_0/libs/context/doc/html/index.html
|
||||||
|
// g++ -std=c++11 -O1 -Wall -g test_cbcoro.cc -pthread && ./a.out
|
||||||
|
|
||||||
|
#include <glim/exception.hpp>
|
||||||
|
#include <glim/NsecTimer.hpp>
|
||||||
|
|
||||||
|
#include "cbcoro.hpp"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h> // sleep
|
||||||
|
#include <string.h> // strerror
|
||||||
|
#include <errno.h>
|
||||||
|
#include <functional>
|
||||||
|
using std::function;
|
||||||
|
#include <thread>
|
||||||
|
#include <memory>
|
||||||
|
using std::shared_ptr; using std::make_shared;
|
||||||
|
#include <string>
|
||||||
|
using std::string; using std::to_string;
|
||||||
|
#include <iostream>
|
||||||
|
using std::cout; using std::endl;
|
||||||
|
|
||||||
|
/** A typical remote service with callback. */
|
||||||
|
void esDelete (int frople, std::function<void(int)> cb) {
|
||||||
|
std::thread th ([cb,frople]() {
|
||||||
|
cout << "esDelete: sleeping for a second" << endl;
|
||||||
|
std::this_thread::sleep_for (std::chrono::seconds (1));
|
||||||
|
cb (frople);
|
||||||
|
}); th.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RemoveFroples: public glim::CBCoro {
|
||||||
|
const char* _argument;
|
||||||
|
RemoveFroples (const char* argument): _argument (argument) {
|
||||||
|
cout << "RF: constructor" << endl;
|
||||||
|
}
|
||||||
|
virtual ~RemoveFroples() {puts ("~RemoveFroples");}
|
||||||
|
virtual void run() override {
|
||||||
|
for (int i = 1; i <= 4; ++i) {
|
||||||
|
cout << "RF: Removing frople " << i << "..." << endl;
|
||||||
|
int returnedFrople = 0;
|
||||||
|
yieldForCallback ([this,i,&returnedFrople]() {
|
||||||
|
if (i != 2) {
|
||||||
|
// Sometimes we use a callback.
|
||||||
|
esDelete (i, [this,&returnedFrople](int frople) {
|
||||||
|
cout << "RF,CB: frople " << frople << "." << endl;
|
||||||
|
returnedFrople = frople;
|
||||||
|
invokeFromCallback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Sometimes we don't use a callback.
|
||||||
|
returnedFrople = 0;
|
||||||
|
invokeFromCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cout << "RF: Returned from callback; _returnTo is: " << (intptr_t) _returnTo << "; frople " << returnedFrople << endl;
|
||||||
|
}
|
||||||
|
cout << "RF: finish! _returnTo is: " << (intptr_t) _returnTo << endl;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
glim::cbCoro ([](glim::CBCoro* cbcoro) {
|
||||||
|
cout << "main: run1, thread " << std::this_thread::get_id() << endl; // Runs on the `main` thread.
|
||||||
|
cbcoro->yieldForCallback ([&]() {
|
||||||
|
std::thread callbackThread ([&]() {
|
||||||
|
std::this_thread::sleep_for (std::chrono::seconds (4));
|
||||||
|
cbcoro->invokeFromCallback();
|
||||||
|
}); callbackThread.detach();
|
||||||
|
});
|
||||||
|
cout << "main: run2, thread " << std::this_thread::get_id() << endl; // Runs on the `callbackThread`.
|
||||||
|
});
|
||||||
|
|
||||||
|
(new RemoveFroples ("argument"))->start();
|
||||||
|
cout << "main: returned from RemoveFroples" << endl;
|
||||||
|
|
||||||
|
glim::NsecTimer timer; const int ops = RUNNING_ON_VALGRIND ? 999 : 9999;
|
||||||
|
for (int i = 0; i < ops; ++i) glim::cbCoro ([](glim::CBCoro* cbcoro) {});
|
||||||
|
double speedEmpty = ops / timer.sec();
|
||||||
|
timer.restart();
|
||||||
|
for (int i = 0; i < ops; ++i) glim::cbCoro ([](glim::CBCoro* cbcoro) {cbcoro->yieldForCallback ([&]() {cbcoro->invokeFromCallback();});});
|
||||||
|
double speedImmediate = ops / timer.sec();
|
||||||
|
|
||||||
|
sleep (5);
|
||||||
|
cout << "speed: empty: " << speedEmpty << " o/s" << endl;
|
||||||
|
cout << "speed: immediate: " << speedImmediate << " o/s" << endl;
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
#define _GLIM_ALL_EXCEPTIONS_CODE
|
||||||
|
#include "exception.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
#include <typeinfo>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
// NB: Controlling exceptions across shared object (.so) boundaries is tested separately in frople/src/test.cpp/testExceptionControl.
|
||||||
|
|
||||||
|
static void testThrowLine() {
|
||||||
|
int line = 0; std::string message; try {
|
||||||
|
line = __LINE__; GTHROW ("message");
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
message = ex.what();
|
||||||
|
}
|
||||||
|
//std::cout << message << ' ' << std::flush;
|
||||||
|
assert (message.size());
|
||||||
|
assert (std::string (message) .find (":" + std::to_string (line)) != std::string::npos);
|
||||||
|
|
||||||
|
line = 0; message.clear(); std::string name; try {
|
||||||
|
line = __LINE__; G_DEFINE_EXCEPTION (FooEx); GNTHROW (FooEx, "foo");
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
message = ex.what(); name = typeid (ex) .name();
|
||||||
|
}
|
||||||
|
//std::cout << "testThrowLine: " << message << ' ' << name << ' ' << std::flush;
|
||||||
|
assert (message.size());
|
||||||
|
assert (std::string (message) .find (":" + std::to_string (line)) != std::string::npos);
|
||||||
|
assert (name.find ("FooEx") != std::string::npos);
|
||||||
|
|
||||||
|
message.clear(); try {
|
||||||
|
glim::ExceptionControl plainWhat (glim::Exception::PLAIN_WHAT);
|
||||||
|
GTHROW ("bar");
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
message = ex.what();
|
||||||
|
}
|
||||||
|
assert (message == "bar");
|
||||||
|
assert (glim::Exception::options() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testBacktrace() {
|
||||||
|
assert (glim::Exception::options() == 0);
|
||||||
|
glim::ExceptionControl captureTrace (glim::Exception::CAPTURE_TRACE);
|
||||||
|
assert (glim::Exception::options() != 0);
|
||||||
|
std::string message;
|
||||||
|
try {
|
||||||
|
GTHROW ("message");
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
message = ex.what();
|
||||||
|
}
|
||||||
|
//std::cout << "testBacktrace: " << message << std::endl;
|
||||||
|
if (message.find ("[at bin/test_exception") == std::string::npos && message.find ("[test_exception") == std::string::npos)
|
||||||
|
GTHROW ("No expected string in " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAllExceptionsHack() {
|
||||||
|
assert (glim::Exception::options() == 0);
|
||||||
|
std::string traceBuf;
|
||||||
|
glim::ExceptionHandler traceExceptions (glim::Exception::HANDLE_ALL | glim::Exception::RENDEZVOUS, glim::captureBacktrace, &traceBuf);
|
||||||
|
assert (glim::Exception::options() != 0);
|
||||||
|
try {
|
||||||
|
throw "catch me"; // Catched by `_GLIM_ALL_EXCEPTIONS_CODE` and handled with `glim::ExceptionControl::backtrace`.
|
||||||
|
} catch (const char* skip) {}
|
||||||
|
//std::cout << "testAllExceptionsHack: " << std::endl << traceBuf << std::endl;
|
||||||
|
assert (traceBuf.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main () {
|
||||||
|
std::cout << "Testing exception.hpp ... " << std::flush;
|
||||||
|
testThrowLine();
|
||||||
|
testBacktrace();
|
||||||
|
testAllExceptionsHack();
|
||||||
|
std::cout << "pass." << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
#include "gstring.hpp"
|
||||||
|
using glim::gstring;
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <sstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
|
#include <boost/algorithm/string/trim.hpp>
|
||||||
|
#include <boost/algorithm/string/case_conv.hpp>
|
||||||
|
|
||||||
|
static void testIterators();
|
||||||
|
static void testBoost();
|
||||||
|
static void testStrftime();
|
||||||
|
|
||||||
|
int main () {
|
||||||
|
std::cout << "Testing gstring.hpp ... " << std::flush;
|
||||||
|
|
||||||
|
gstring gs;
|
||||||
|
if (gs.needsFreeing()) throw std::runtime_error ("Default gstring needsFreeing");
|
||||||
|
if (gs.capacity() != 1) throw std::runtime_error ("Default gstring capacity is not 1");
|
||||||
|
char buf16[16];
|
||||||
|
gstring gs16 (sizeof (buf16), buf16, false, 0);
|
||||||
|
if (gs16.capacity() != 16) throw std::runtime_error ("gs16 capacity != 16");
|
||||||
|
if (gs16.size() != 0) throw std::runtime_error ("gs16 size != 0");
|
||||||
|
gstring gsFree (17, NULL, true, 0);
|
||||||
|
if (!gsFree.needsFreeing()) throw std::runtime_error ("!needsFreeing");
|
||||||
|
if (gsFree.capacity() != 16) throw std::runtime_error ("gsFree capacity != 16");
|
||||||
|
if (gsFree.size() != 0) throw std::runtime_error ("gsFree size != 0");
|
||||||
|
gstring gsRO (0, NULL, false, 0);
|
||||||
|
if (gsRO.needsFreeing()) throw std::runtime_error ("needsFreeing");
|
||||||
|
if (gsRO.capacity() != 1) throw std::runtime_error ("gsRO capacity != 1");
|
||||||
|
if (gsRO.size() != 0) throw std::runtime_error ("gsRO size != 0");
|
||||||
|
char buf32[32];
|
||||||
|
gstring gs32 (sizeof (buf32), buf32, false, 0);
|
||||||
|
if (gs32.capacity() != 32) throw std::runtime_error ("capacity != 32");
|
||||||
|
if (gs32.size() != 0) throw std::runtime_error ("gs32 size != 0");
|
||||||
|
const gstring foo = C2GSTRING ("foo");
|
||||||
|
if (foo.needsFreeing()) throw std::runtime_error ("foo needsFreeing");
|
||||||
|
if (foo != "foo") throw std::runtime_error ("foo != foo");
|
||||||
|
if (foo.size() != 3) throw std::runtime_error ("foo not 3");
|
||||||
|
std::ostringstream oss; oss << gs16 << gsFree << gsRO << gs32 << foo;
|
||||||
|
if (oss.str() != "foo") throw std::runtime_error ("oss foo != foo");
|
||||||
|
glim::gstring_stream gss (gs16); std::ostream gsos (&gss);
|
||||||
|
gsos << "bar" << std::flush;
|
||||||
|
if (gs16 != "bar") throw std::runtime_error ("gs16 != bar");
|
||||||
|
gsos << "beer" << std::flush;
|
||||||
|
if (gs16 != "barbeer") throw std::runtime_error ("gs16 != barbeer");
|
||||||
|
gsos << "123456789" << std::flush;
|
||||||
|
if (gs16 != "barbeer123456789") throw std::runtime_error ("gs16 != barbeer123456789");
|
||||||
|
if (gs16.capacity() != 16) throw std::runtime_error ("gs16 != 16");
|
||||||
|
gsos << '0' << std::flush;
|
||||||
|
if (gs16 != "barbeer1234567890") throw std::runtime_error ("gs16 != barbeer1234567890");
|
||||||
|
if (gs16.capacity() != 32) throw std::runtime_error ("gs16 != 32");
|
||||||
|
|
||||||
|
gstring gsb; std::string str ("abc");
|
||||||
|
gsb << 'a' << 1 << 2LL << str;
|
||||||
|
std::string ns ("1:3,"); std::istringstream nsi (ns);
|
||||||
|
gsb.readNetstring (nsi);
|
||||||
|
if (gsb != "a12abc3") throw std::runtime_error ("gsb != a12abc3");
|
||||||
|
if (strcmp (gsb.c_str(), "a12abc3") != 0) throw std::runtime_error ("strcmp ! 0");
|
||||||
|
|
||||||
|
gsb.clear().appendNetstring ("foo") .appendNetstring ("bar");
|
||||||
|
if (gsb != "3:foo,3:bar,") throw std::runtime_error ("gsb != 3:foo,3:bar,");
|
||||||
|
uint32_t pos = 0;
|
||||||
|
if (gsb.netstringAt (pos, &pos) != "foo" || gsb.netstringAt (pos, &pos) != "bar" || pos != gsb.length())
|
||||||
|
throw std::runtime_error ("gsb !netstringAt");
|
||||||
|
|
||||||
|
gs32.clear() << 12345 << ',';
|
||||||
|
if (gs32.intAt (0, &pos) != 12345 || pos != 5) throw std::runtime_error ("gsb !12345");
|
||||||
|
if (gs32.intAt (1, &pos) != 2345 || pos != 5) throw std::runtime_error ("gsb !2345");
|
||||||
|
if (gs32.intAt (5, &pos) != 0 || pos != 5) throw std::runtime_error ("gsb !0");
|
||||||
|
|
||||||
|
if ((gs32.clear() << 123).erase (0) != "23") throw std::runtime_error ("!23");
|
||||||
|
if ((gs32.clear() << 123).erase (1) != "13") throw std::runtime_error ("!13");
|
||||||
|
if ((gs32.clear() << 123).erase (2) != "12") throw std::runtime_error ("!12");
|
||||||
|
|
||||||
|
std::unordered_map<glim::gstring, int> map;
|
||||||
|
map[glim::gstring ("foo")] = 1;
|
||||||
|
glim::gstring bar ("bar");
|
||||||
|
map[bar] = 1;
|
||||||
|
map[glim::gstring ("sum")] = map[glim::gstring ("foo")] + map[glim::gstring ("bar")];
|
||||||
|
if (map[glim::gstring ("sum")] != 2) throw std::runtime_error ("sum != 2");
|
||||||
|
map.clear();
|
||||||
|
|
||||||
|
gstring gs1 ("foo"); gstring gs2 ("bar");
|
||||||
|
gs1 = gstring (gs2 << "_"); // Copying in order to malloc length() bytes.
|
||||||
|
if (gs1 != "bar_") throw std::runtime_error ("!bar_");
|
||||||
|
if (gs1.capacity() != 1) throw std::runtime_error ("bar_ != 4");
|
||||||
|
|
||||||
|
testIterators();
|
||||||
|
testBoost();
|
||||||
|
testStrftime();
|
||||||
|
|
||||||
|
std::cout << "pass." << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testIterators() {
|
||||||
|
gstring foo (C2GSTRING ("foo"));
|
||||||
|
gstring buf; for (auto it = foo.begin(), end = foo.end(); it != end; ++it) buf << *it;
|
||||||
|
assert (buf == "foo");
|
||||||
|
assert (boost::starts_with (foo, "f") && boost::ends_with (foo, "oo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testBoost() {
|
||||||
|
gstring str (" foo\t\r\n");
|
||||||
|
boost::trim (str);
|
||||||
|
assert (str == "foo");
|
||||||
|
|
||||||
|
gstring up ("FOO"); boost::to_lower (up);
|
||||||
|
assert (up == "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testStrftime() {
|
||||||
|
time_t tim = time(0); struct tm ltime; memset (<ime, 0, sizeof ltime); if (!localtime_r (&tim, <ime)) GTHROW ("!localtime_r");
|
||||||
|
GSTRING_ON_STACK (t1, 8); assert (t1.capacity() == 8);
|
||||||
|
t1.appendTime ("", <ime); assert (t1 == "");
|
||||||
|
t1.appendTime ("foo %a, %d %b %Y %T %z bar", <ime);
|
||||||
|
assert (t1.capacity() > 8); // Capacity increased to account for the large string.
|
||||||
|
assert (boost::starts_with (t1, "foo "));
|
||||||
|
assert (boost::ends_with (t1, " bar"));
|
||||||
|
|
||||||
|
GSTRING_ON_STACK (t2, 8); assert (t2.capacity() == 8);
|
||||||
|
t2.appendTime ("%H", <ime);
|
||||||
|
assert (t2.capacity() == 8); // 8 is big enought, isn't it?
|
||||||
|
assert (t2.intAt (0) == ltime.tm_hour); // NB: intAt is safe here because strftime adds an uncounted null-terminator.
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
#include "ldb.hpp"
|
||||||
|
using glim::Ldb;
|
||||||
|
using glim::gstring;
|
||||||
|
#include <iostream>
|
||||||
|
using std::cout; using std::flush; using std::endl;
|
||||||
|
#include <assert.h>
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
|
void test1 (Ldb& ldb) {
|
||||||
|
ldb.put (std::string ("foo_"), std::string ("bar"));
|
||||||
|
ldb.put ((uint32_t) 123, 1);
|
||||||
|
ldb.put ((uint32_t) 123, 2);
|
||||||
|
ldb.put (C2GSTRING ("foo"), 3);
|
||||||
|
ldb.put (C2GSTRING ("foo"), 4);
|
||||||
|
ldb.put (C2GSTRING ("gsk"), C2GSTRING ("gsv"));
|
||||||
|
std::string ts; int ti; gstring tgs;
|
||||||
|
auto fail = [](std::string msg) {throw std::runtime_error ("assertion failed: " + msg);};
|
||||||
|
if (!ldb.get (std::string ("foo_"), ts) || ts != "bar") fail ("!foo_=bar");
|
||||||
|
if (!ldb.get ((uint32_t) 123, ti) || ti != 2) fail ("!123=2");
|
||||||
|
if (!ldb.get (C2GSTRING ("foo"), ti) || ti != 4) fail ("!foo=4");
|
||||||
|
if (!ldb.get (C2GSTRING ("gsk"), tgs) || tgs != "gsv") fail ("!gsk=gsv");
|
||||||
|
|
||||||
|
// Test range-based for.
|
||||||
|
int count = 0; bool haveGskGsv = false;
|
||||||
|
for (auto&& entry: ldb) {
|
||||||
|
if (!entry._lit->Valid()) fail ("!entry");
|
||||||
|
if (entry.keyView() == "gsk") {
|
||||||
|
if (entry.getKey<gstring>() != "gsk") fail ("getKey(gsk)!=gsk");
|
||||||
|
if (entry.getValue<gstring>() != "gsv") fail ("getValue(gsk)!=gsv");
|
||||||
|
haveGskGsv = true;
|
||||||
|
}
|
||||||
|
++count;}
|
||||||
|
if (count != 4) fail ("count!=4"); // foo_=bar, 123=2, foo=4, gsk=gsv
|
||||||
|
if (!haveGskGsv) fail ("!haveGskGsv");
|
||||||
|
|
||||||
|
ldb.del ((uint32_t) 123); if (ldb.get ((uint32_t) 123, ti)) fail ("123");
|
||||||
|
ldb.del (C2GSTRING ("foo")); if (ldb.get (C2GSTRING ("foo"), ti)) fail ("foo");
|
||||||
|
ldb.del (std::string ("foo_"));
|
||||||
|
|
||||||
|
{ // We've erased "123" and "foo", the only key left is "gsk" (gsk=gsv), let's test the iterator boundaries on this small dataset.
|
||||||
|
auto&& it = ldb.begin();
|
||||||
|
if (it->getKey<gstring>() != "gsk") fail ("first key !gsk " + it->keyView().str());
|
||||||
|
if (!(++it).end()) fail ("++it != end");
|
||||||
|
if ((--it)->getKey<gstring>() != "gsk") fail ("can't go back to gsk");
|
||||||
|
if (!(--it).end()) fail ("--it != end");
|
||||||
|
if ((++it)->getKey<gstring>() != "gsk") fail ("can't go forward to gsk");
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: index trigger example
|
||||||
|
// struct SimpleIndexTrigger: public Trigger { // Uses key space partitioning (cf. http://stackoverflow.com/a/12503799/257568)
|
||||||
|
// const char* _name; Ldb _indexDb;
|
||||||
|
// SimpleIndexTrigger (Ldb& ldb, const char* name = "index"): _name (name), _indexDb (ldb._env, name) {}
|
||||||
|
// gstring triggerName() {return gstring (0, (void*) _name, false, strlen (_name), true);}
|
||||||
|
// void add (Ldb& ldb, void* key, gstring& kbytes, void* value, gstring& vbytes, Transaction& txn) {
|
||||||
|
// MDB_val mkey = {vbytes.size(), (void*) vbytes.data()};
|
||||||
|
// MDB_val mvalue = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
// int rc = ::ldb_put (txn.get(), _indexDb._dbi, &mkey, &mvalue, 0);
|
||||||
|
// if (rc) GNTHROW (LdbEx, std::string ("index, ldb_put: ") + ::strerror (rc));
|
||||||
|
// }
|
||||||
|
// void erase (Ldb& ldb, void* ekey, gstring& kbytes, Transaction& txn) {
|
||||||
|
// // Get all the values and remove them from the index.
|
||||||
|
// MDB_cursor* cur = 0; int rc = ::ldb_cursor_open (txn.get(), ldb._dbi, &cur);
|
||||||
|
// if (rc) GNTHROW (LdbEx, std::string ("index, erase, ldb_cursor_open: ") + ::strerror (rc));
|
||||||
|
// std::unique_ptr<MDB_cursor, void(*)(MDB_cursor*)> curHolder (cur, ::ldb_cursor_close);
|
||||||
|
// MDB_val mkey = {kbytes.size(), (void*) kbytes.data()}, val = {0, 0};
|
||||||
|
// rc = ::ldb_cursor_get (cur, &mkey, &val, ::MDB_SET_KEY); if (rc == MDB_NOTFOUND) return;
|
||||||
|
// if (rc) GNTHROW (LdbEx, std::string ("index, erase, ldb_cursor_get: ") + ::strerror (rc));
|
||||||
|
// rc = ::ldb_del (txn.get(), _indexDb._dbi, &val, &mkey);
|
||||||
|
// if (rc && rc != MDB_NOTFOUND) GNTHROW (LdbEx, std::string ("index, erase, ldb_del: ") + ::strerror (rc));
|
||||||
|
// for (;;) {
|
||||||
|
// rc = ::ldb_cursor_get (cur, &mkey, &val, ::MDB_NEXT_DUP); if (rc == MDB_NOTFOUND) return;
|
||||||
|
// if (rc) GNTHROW (LdbEx, std::string ("index, erase, ldb_cursor_get: ") + ::strerror (rc));
|
||||||
|
// rc = ::ldb_del (txn.get(), _indexDb._dbi, &val, &mkey);
|
||||||
|
// if (rc && rc != MDB_NOTFOUND) GNTHROW (LdbEx, std::string ("index, erase, ldb_del: ") + ::strerror (rc));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// void eraseKV (Ldb& ldb, void* key, gstring& kbytes, void* value, gstring& vbytes, Transaction& txn) {
|
||||||
|
// MDB_val mkey = {vbytes.size(), (void*) vbytes.data()};
|
||||||
|
// MDB_val mvalue = {kbytes.size(), (void*) kbytes.data()};
|
||||||
|
// int rc = ::ldb_del (txn.get(), _indexDb._dbi, &mkey, &mvalue);
|
||||||
|
// if (rc && rc != MDB_NOTFOUND) GNTHROW (LdbEx, std::string ("index, ldb_del: ") + ::strerror (rc));
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// auto indexTrigger = std::make_shared<SimpleIndexTrigger> (ldb); ldb.setTrigger (indexTrigger); auto& indexDb = indexTrigger->_indexDb;
|
||||||
|
// ldb.erase (C2GSTRING ("gsk")); // NB: "gsk" wasn't indexed here. `IndexTrigger.erase` should handle this gracefully.
|
||||||
|
|
||||||
|
// Add indexed.
|
||||||
|
// ldb.put (C2GSTRING ("ik"), C2GSTRING ("iv1"));
|
||||||
|
// ldb.put (C2GSTRING ("ik"), string ("iv2"));
|
||||||
|
// ldb.put (C2GSTRING ("ik"), 3);
|
||||||
|
// Check the index.
|
||||||
|
// gstring ik;
|
||||||
|
// if (!indexDb.first (C2GSTRING ("iv1"), ik) || ik != "ik") fail ("!iv1=ik");
|
||||||
|
// if (!indexDb.first (string ("iv2"), ik) || ik != "ik") fail ("!iv2=ik");
|
||||||
|
// if (!indexDb.first (3, ik) || ik != "ik") fail ("!iv3=ik");
|
||||||
|
|
||||||
|
// Remove indexed.
|
||||||
|
// ldb.eraseKV (C2GSTRING ("ik"), string ("iv2"));
|
||||||
|
// Check the index.
|
||||||
|
// if (!indexDb.first (C2GSTRING ("iv1"), ik) || ik != "ik") fail ("!iv1=ik");
|
||||||
|
// if (indexDb.first (string ("iv2"), ik)) fail ("iv2=ik");
|
||||||
|
// if (!indexDb.first (3, ik) || ik != "ik") fail ("!iv3=ik");
|
||||||
|
|
||||||
|
// Remove indexed.
|
||||||
|
// ldb.erase (C2GSTRING ("ik"));
|
||||||
|
// Check the index.
|
||||||
|
// if (indexDb.first (C2GSTRING ("iv1"), ik)) fail ("iv1");
|
||||||
|
// if (indexDb.first (3, ik)) fail ("iv3");
|
||||||
|
// Check the data.
|
||||||
|
// if (ldb.first (C2GSTRING ("ik"), ik)) fail ("ik");
|
||||||
|
}
|
||||||
|
|
||||||
|
void testStartsWith (Ldb& ldb) {
|
||||||
|
// Using `gstring`s because the Boost Serialization encoding for CStrings is not prefix-friendly.
|
||||||
|
ldb.put (C2GSTRING ("01"), ""); ldb.put (C2GSTRING ("02"), "");
|
||||||
|
ldb.put (C2GSTRING ("11"), "");
|
||||||
|
ldb.put (C2GSTRING ("21"), ""); ldb.put (C2GSTRING ("222"), ""); ldb.put (C2GSTRING ("2"), "");
|
||||||
|
|
||||||
|
auto range = ldb.startsWith (C2GSTRING ("0")); auto it = range.begin();
|
||||||
|
assert (it->keyView() == "01"); assert (it != range.end());
|
||||||
|
assert ((++it)->keyView() == "02"); assert (it != range.end());
|
||||||
|
assert ((++it)->keyView().empty()); assert (it == range.end());
|
||||||
|
assert ((--it)->keyView() == "02"); assert (it != range.end());
|
||||||
|
assert ((--it)->keyView() == "01"); assert (it != range.end());
|
||||||
|
assert ((--it)->keyView().empty()); assert (it == range.end());
|
||||||
|
// `it` and `range.begin` point to the same `leveldb::Iterator`.
|
||||||
|
assert (range.begin()._entry->_lit == it._entry->_lit);
|
||||||
|
assert (!range.begin()._entry->_valid); assert (range.begin()->keyView().empty());
|
||||||
|
|
||||||
|
range = ldb.startsWith (C2GSTRING ("0")); it = range.end();
|
||||||
|
assert (it.end() && it->keyView().empty()); assert (it != range.begin());
|
||||||
|
assert ((--it)->keyView() == "02"); assert (it != range.begin());
|
||||||
|
assert ((--it)->keyView() == "01"); assert (it == range.begin());
|
||||||
|
assert ((--it)->keyView().empty()); assert (it != range.begin());
|
||||||
|
|
||||||
|
int8_t count = 0; for (auto& en: ldb.startsWith (C2GSTRING ("1"))) {en.keyView(); ++count;} assert (count == 1);
|
||||||
|
count = 0; for (auto& en: ldb.startsWith (C2GSTRING ("2"))) {en.keyView(); ++count;} assert (count == 3);
|
||||||
|
count = 0; for (auto& en: ldb.startsWith (C2GSTRING ("-"))) {en.keyView(); ++count;} assert (count == 0);
|
||||||
|
count = 0; for (auto& en: ldb.startsWith (C2GSTRING (""))) {en.keyView(); ++count;} assert (count == 6);
|
||||||
|
|
||||||
|
assert (ldb.startsWith (C2GSTRING ("-")) .empty());
|
||||||
|
|
||||||
|
count = 0; for (auto& en: boost::make_iterator_range (ldb.end().seek ("1"), ldb.end().seek ("2"))) {en.keyView(); ++count;} assert (count == 1);
|
||||||
|
count = 0; for (auto& en: boost::make_iterator_range (ldb.end().seek ("2"), ldb.end().seek ("3"))) {en.keyView(); ++count;} assert (count == 3);
|
||||||
|
count = 0; for (auto& en: ldb.range (C2GSTRING ("1"), C2GSTRING ("2"))) {en.keyView(); ++count;} assert (count == 1);
|
||||||
|
|
||||||
|
{ auto range = ldb.range (C2GSTRING ("0"), C2GSTRING ("1")); // 01 and 02, but not 11.
|
||||||
|
count = 0; for (auto& en: range) {en.keyView(); ++count;} assert (count == 2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
cout << "Testing ldb.hpp ... " << flush;
|
||||||
|
boost::filesystem::remove_all ("/dev/shm/ldbTest");
|
||||||
|
|
||||||
|
Ldb ldb ("/dev/shm/ldbTest");
|
||||||
|
test1 (ldb);
|
||||||
|
|
||||||
|
for (auto& en: ldb) ldb.del (en.keyView());
|
||||||
|
testStartsWith (ldb);
|
||||||
|
|
||||||
|
ldb._db.reset(); // Close.
|
||||||
|
boost::filesystem::remove_all ("/dev/shm/ldbTest");
|
||||||
|
cout << "pass." << endl;
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
#include "runner.hpp"
|
||||||
|
#include "curl.hpp"
|
||||||
|
#include <future>
|
||||||
|
#include <thread>
|
||||||
|
#include <iostream>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
static void testRunner() {
|
||||||
|
using glim::RunnerV2;
|
||||||
|
auto runner = RunnerV2::instance();
|
||||||
|
|
||||||
|
struct Dugout: public glim::CurlmInformationListener {
|
||||||
|
glim::Curl _curl;
|
||||||
|
std::promise<std::string> _promise;
|
||||||
|
void schedule (RunnerV2* runner) {
|
||||||
|
_curl.http ("http://glim.ru/", 2);
|
||||||
|
curl_easy_setopt (_curl._curl, CURLOPT_PRIVATE, this); // Tells `addToCURLM` to call this listener later.
|
||||||
|
runner->addToCURLM (_curl._curl);
|
||||||
|
}
|
||||||
|
virtual FreeOptions information (CURLMsg* msg, CURLM* curlm) override {
|
||||||
|
_promise.set_value (_curl.str());
|
||||||
|
return static_cast<FreeOptions> (REMOVE_CURL_FROM_CURLM | DELETE_LISTENER);
|
||||||
|
}
|
||||||
|
virtual ~Dugout() {}
|
||||||
|
};
|
||||||
|
{ Dugout* dugout = new Dugout();
|
||||||
|
auto future = dugout->_promise.get_future();
|
||||||
|
dugout->schedule (runner.get());
|
||||||
|
if (future.get().find ("<html") == std::string::npos) GTHROW ("!html"); }
|
||||||
|
|
||||||
|
auto curl = std::make_shared<glim::Curl>(); curl->http ("http://glim.ru/", 2);
|
||||||
|
auto promise = std::make_shared<std::promise<std::string>>(); auto future = promise->get_future();
|
||||||
|
runner->addToCURLM (curl->_curl, [curl,promise](CURLMsg*, CURLM*) {promise->set_value (curl->str());});
|
||||||
|
if (future.get().find ("<html") == std::string::npos) GTHROW ("!html");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void oldTest() {
|
||||||
|
std::shared_ptr<struct event_base> evbase (event_base_new(), event_base_free);
|
||||||
|
glim::Runner runner (evbase, [](const char* error) {std::cerr << error << std::endl;});
|
||||||
|
|
||||||
|
auto scheduledJobFired = std::make_shared<bool> (false);
|
||||||
|
runner.schedule (0.f, [=](glim::Runner::JobInfo&)->bool {*scheduledJobFired = true; return false;});
|
||||||
|
|
||||||
|
auto curl = std::make_shared<glim::Curl> (false); curl->http ("http://glim.ru/env.cgi?pause=50", 5);
|
||||||
|
auto curlDebug = std::make_shared<std::string>(); curl->debugListenerF ([curlDebug](const char* bytes, size_t size) {curlDebug->append (bytes, size);});
|
||||||
|
volatile bool ran = false;
|
||||||
|
runner.multi (curl->_curl, [curl,&ran,evbase,curlDebug](CURLMsg* msg) {
|
||||||
|
std::cout << " status: " << curl->status();
|
||||||
|
if (curl->status() == 200) std::cout << " ip: " << curl->gstr().view (0, std::max (curl->gstr().find ("\n"), 0));
|
||||||
|
if (curlDebug->find ("GET /env.cgi") == std::string::npos) std::cerr << " No headers in debug? " << *curlDebug << std::endl;
|
||||||
|
ran = true;
|
||||||
|
event_base_loopbreak (evbase.get());
|
||||||
|
});
|
||||||
|
//struct timeval tv {1, 0}; event_base_loopexit (evbase.get(), &tv); // Exit the loop in a sec.
|
||||||
|
event_base_dispatch (evbase.get());
|
||||||
|
if (!ran) GTHROW ("!ran");
|
||||||
|
if (!*scheduledJobFired) GTHROW ("!scheduledJobFired");
|
||||||
|
|
||||||
|
std::cout << " pass." << std::endl;
|
||||||
|
//waiting: "was introduced in Libevent 2.1.1-alpha"//libevent_global_shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main () {
|
||||||
|
std::cout << "Testing runner.hpp ..." << std::flush; try {
|
||||||
|
|
||||||
|
testRunner();
|
||||||
|
oldTest();
|
||||||
|
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
std::cerr << " exception: " << ex.what() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
#include "sqlite.hpp"
|
||||||
|
#define S(cstr) (std::pair<char const*, int> (cstr, sizeof (cstr) - 1))
|
||||||
|
#include <assert.h>
|
||||||
|
#include <iostream>
|
||||||
|
using namespace glim;
|
||||||
|
|
||||||
|
int main () {
|
||||||
|
std::cout << "Testing sqlite.hpp ... " << std::flush;
|
||||||
|
Sqlite sqlite (":memory:");
|
||||||
|
SqliteSession sqs (&sqlite);
|
||||||
|
assert (sqs.query ("CREATE TABLE test (t TEXT, i INTEGER)") .ustep() == 0);
|
||||||
|
assert (sqs.query ("INSERT INTO test VALUES (?, ?)") .bind (1, S("foo")) .bind (2, 27) .ustep() == 1);
|
||||||
|
assert (sqs.query ("SELECT t FROM test") .qstep() .stringAt (1) == "foo");
|
||||||
|
assert (sqs.query ("SELECT i FROM test") .qstep() .intAt (1) == 27);
|
||||||
|
std::cout << "pass." << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
Loading…
Reference in New Issue