Сегодня я хочу продолжить описание процесса разработки новой версии Marmalade Framework. В этой части я расскажу о создании подсистемы загрузки конфигурации, поддерживающей формат описания, о котором я рассказывал ранее. Основной сложностью, с которой придется столкнуться на этом шаге, будет поддержка шаблонов (templates), в том числе и локальных.
Итак, нашей сегодняшней задачей станет загрузка следующего описания:
{ load: state.json,
display: { width: 720,
orientation: portrait
},
templates: { wt_N: { action: w_N,
name: wr_N,
template: lock,
x: 1,
template: y,
width: -1,
height: 30,
image: w_N
},
worlds: { regions: [ { templates: { y: { y: 1 } },
template: wt_1
},
{ templates: { y: { y: 31 } },
templates: { lock: { lock: stars>=10 } },
template: wt_1
}
]
}
},
scopes: { start: { default: Y,
vars: { type: number,
stars: 0
},
vars: { background: world_frame,
back: quit
},
regions: [ { zorder: -1,
x: 10,
y: 10,
width: -10,
height: -10,
template: worlds
}
]
},
w_N: { load: w_N.json,
game: arcanoid,
vars: { type: number,
life: 5,
score: 0
},
vars: { background: b_N,
back: back
},
set: { event: impact,
score: score+10
}
}
}
}
Разумеется, здесь присутствует далеко не все, что потребуется для создания работоспособной программы, но этого описания вполне достаточно для тестирования корректности работы подсистемы загрузки (приложение, выполняющее загрузку этого описания будет включено в проект в качестве функционального теста).
Для хранения этих данных в оперативной памяти будет использоваться следующая иерархия классов:
Все классы, принимающие данные из описания, являются наследниками AbstractLoader. Описание состоит из набора именованных независимых ScopeDef (описаний состояний), каждое из которых содержит коллекции RegionDef (описаний экранных областей) и VarDef (описаний переменных). RegionDef также содержат коллекции вложенных RegionDef и VarDef. Template является служебным классом и используется только во время загрузки описания.
Прежде чем двигаться дальше, следует рассказать о проблеме, возникшей у меня с использованием строк. Возможно я что-то делал (и продолжаю делать) не так, но std::string разрушают мне память при копировании их значений. Некоторое время я пытался с этим бороться, но потом решил, что мне нужен свой тип для хранения строк, не столь интенсивно копирующий их содержимое при использовании. Кроме того, поскольку вопрос экономии памяти на мобильных платформах стоит остро, хотелось бы не хранить в памяти более одного экземпляра каждого строкового значения. В результате, у меня получилась следующая реализация пула строк:
#ifndef _STRING_POOL_H_
#define _STRING_POOL_H_
#include <stdio.h>
#include <string.h>
#include <map>
using namespace std;
namespace mf2 {
class StringPool {
private:
struct Value {
Value(const char* s): strValue(s) {}
Value(const Value& p): strValue(p.strValue) {}
bool operator<(const Value& p) const {return strcmp(strValue, p.strValue) < 0;}
const char* strValue;
};
map<Value, int>* usage;
public:
void init();
void release();
bool freeString(const char* s);
const char* getString(const char* s);
const char* getString(char c);
typedef map<Value, int>::iterator VIter;
typedef pair<Value, int> VPair;
};
extern StringPool pool;
}
#endif // _STRING_POOL_H_
#include "StringPool.h"
namespace mf2 {
StringPool pool;
void StringPool::init(){
usage = new map<Value, int>();
}
void StringPool::release() {
for (VIter p = usage->begin(); p != usage->end(); ++p) {
delete [] p->first.strValue;
}
delete usage;
}
bool StringPool::freeString(const char* s) {
VIter p = usage->find(Value(s));
if (p != usage->end()) {
if (--p->second <= 0) {
delete [] p->first.strValue;
usage->erase(p);
}
return true;
}
return false;
}
const char* StringPool::getString(const char* s) {
if (s == NULL) return s;
VIter p = usage->find(Value(s));
if (p != usage->end()) {
p->second++;
return p->first.strValue;
}
int sz = strlen(s) + 1;
char* r = new char[sz];
memset(r, 0, sz);
strcpy(r, s);
usage->insert(VPair(Value(r), 1));
return r;
}
const char* StringPool::getString(char c) {
char s[2];
s[0] = c; s[1] = 0;
return getString(s);
}
}
Для хранения строковых значений используется следующий класс:
#ifndef _STRING_VALUE_H_
#define _STRING_VALUE_H_
#include "s3eTypes.h"
namespace mf2 {
class StringValue {
const char* strValue;
public:
StringValue(): strValue(NULL) {}
StringValue(char c);
StringValue(const char* s);
StringValue(const StringValue& p);
~StringValue();
bool isString() const {return true;}
bool isNull() const {return (strValue == NULL);}
void clear();
const char* getString() const;
bool operator<(const StringValue& p) const;
bool operator>(const StringValue& p) const;
bool operator<=(const StringValue& p) const {return !(*this > p);}
bool operator>=(const StringValue& p) const {return !(*this < p);}
bool operator!=(const StringValue& p) const {return (*this < p) || (*this > p);}
bool operator==(const StringValue& p) const {return !(*this != p);}
StringValue& operator=(const StringValue& p);
};
}
#endif // _STRING_VALUE_H_
#include "StringValue.h"
#include "StringPool.h"
namespace mf2 {
StringValue::StringValue(const char* s): strValue(pool.getString(s)) {}
StringValue::StringValue(const StringValue& p): strValue(pool.getString(p.strValue)) {}
StringValue::StringValue(char c): strValue(pool.getString(c)) {}
StringValue::~StringValue() {
clear();
}
const char* StringValue::getString() const {
if (isNull()) {
return "";
} else {
return strValue;
}
}
void StringValue::clear() {
if (strValue != NULL) {
pool.freeString(strValue);
strValue = NULL;
}
}
bool StringValue::operator<(const StringValue& p) const {
if (isNull()) {
if (p.isNull()) return false;
else return true;
}
return strcmp(getString(), p.getString()) < 0;
}
bool StringValue::operator>(const StringValue& p) const {
if (p.isNull()) {
if (isNull()) return false;
else return true;
}
return strcmp(getString(), p.getString()) > 0;
}
StringValue& StringValue::operator=(const StringValue& p) {
if (this != &p) {
strValue = pool.getString(p.strValue);
}
return *this;
}
}
Теперь можно приступить собственно к загрузке. Как я уже говорил выше, все классы, принимающие загружаемые данные, будут наследниками следующего абстрактного класса:
#ifndef _ABSTRACT_CONFIGURABLE_H_
#define _ABSTRACT_CONFIGURABLE_H_
#include "s3eTypes.h"
namespace mf2 {
class AbstractConfigurable {
private:
int state;
protected:
virtual AbstractConfigurable* createContext(const char* name) {return NULL;}
virtual bool setValue(const char* name, const char* value) = 0;
virtual bool closeContext() {return true;}
public:
int getState() const {return state;}
void setState(int s) {state = s;}
friend class Loader;
friend class Template;
};
}
#endif // _ABSTRACT_CONFIGURABLE_H_
Здесь нет ничего особенного. Методы createContext, closeContext и setValue будут вызываться загрузчиком в процессе обработки событий разбора JSON-описания. Объект, получивший эти события, сможет либо обработать их самостоятельно, либо делегировать их обработку другому объекту (ниже мы рассмотрим как это делается). В процессе обработки потока событий разбора, полезно также иметь возможность изменять состояние объекта, выполняющего разбор. Для этого, мы определим переменную state и методы для получения и изменения ее значения.
Наиболее сложной частью, связанной с обработкой шаблонов, займется класс Loader:
#ifndef _LOADER_H_
#define _LOADER_H_
#include <stack>
#include <map>
#include <yaml.h>
#include "AbstractConfigurable.h"
#include "Template.h"
#include "StringValue.h"
#define MAX_NAME_SZ 80
#define TEMPLATES_SCOPE "templates"
#define TEMPLATE_PROPERTY "template"
using namespace std;
namespace mf2 {
class Loader: public AbstractConfigurable {
private:
enum {
stTop = 0,
stTemplates = 1,
stTemplate = 2
};
struct Scope {
Scope(AbstractConfigurable* c, int s = 0): ctx(c), state(s) {}
Scope(const Scope& p): ctx(p.ctx), state(p.state) {}
AbstractConfigurable* ctx;
int state;
};
yaml_parser_t parser;
yaml_event_t evnt;
stack<Scope> scopes;
map<StringValue, Template*> templates;
int deep;
char currentName[MAX_NAME_SZ];
void clearCurrentName();
bool notify(AbstractConfigurable* ctx);
virtual AbstractConfigurable* createContext(const char* name);
virtual bool setValue(const char* name, const char* value);
virtual bool closeContext();
void dropTemplates(int level = 0);
bool applyTemplate(const char* name);
public:
Loader();
~Loader();
bool load(const char* name, AbstractConfigurable* ctx);
typedef map<StringValue, Template*>::iterator TIter;
typedef pair<StringValue, Template*> TPair;
};
}
#endif // _LOADER_H_
#include "Loader.h"
#include <string.h>
namespace mf2 {
Loader::Loader(): scopes(), templates(), deep(0) {}
Loader::~Loader() {
dropTemplates();
}
void Loader::clearCurrentName() {
memset(currentName, 0, sizeof(currentName));
}
bool Loader::load(const char* name, AbstractConfigurable* ctx) {
bool r = true;
setState(stTop);
scopes.push(Scope(ctx, ctx->getState()));
clearCurrentName();
yaml_parser_initialize(&parser);
FILE* input = fopen(name, "rb");
if (input != NULL) {
yaml_parser_set_input_file(&parser, input);
int done = 0;
while (!done) {
if (!yaml_parser_parse(&parser, &evnt)) {
r = false;
}
if (!notify(ctx)) {
r = false;
}
if (!r) break;
done = (evnt.type == YAML_STREAM_END_EVENT);
yaml_event_delete(&evnt);
}
fclose(input);
} else {
r = true;
}
yaml_parser_delete(&parser);
return r;
}
bool Loader::notify(AbstractConfigurable* ctx) {
bool r = true;
switch (evnt.type) {
case YAML_MAPPING_START_EVENT:
case YAML_SEQUENCE_START_EVENT:
r = (createContext(currentName) != NULL);
clearCurrentName();
break;
case YAML_MAPPING_END_EVENT:
case YAML_SEQUENCE_END_EVENT:
if (!closeContext()) return false;
break;
case YAML_SCALAR_EVENT:
if (currentName[0] == 0) {
strncpy(currentName, (const char*)evnt.data.scalar.value,
sizeof(currentName) - 1);
break;
}
r = setValue(currentName, (const char*)evnt.data.scalar.value);
clearCurrentName();
break;
default:
break;
}
return r;
}
void Loader::dropTemplates(int level) {
for (bool f = true; f;) {
f = false;
for (TIter p = templates.begin(); p != templates.end(); ++p) {
if (p->second->getLevel() > level) {
f = true;
Template* next = p->second->getNext();
delete p->second;
if (next == NULL) {
templates.erase(p);
} else {
p->second = next;
}
break;
}
}
}
}
AbstractConfigurable* Loader::createContext(const char* name) {
if (scopes.empty()) return NULL;
AbstractConfigurable* r = (AbstractConfigurable*)this;
if (getState() != stTemplate) {
if (strcmp(name, TEMPLATES_SCOPE) == 0) {
scopes.push(Scope(r, stTemplates));
return r;
}
if ((scopes.top().ctx == r) && (scopes.top().state == stTemplates)) {
StringValue nm(name);
TIter p = templates.find(nm);
if (p != templates.end()) {
r = p->second = new Template(name, p->second, deep++);
} else {
Template* t = new Template(name, NULL, deep++);
templates.insert(TPair(nm, t));
r = (AbstractConfigurable*)t;
}
setState(stTemplate);
scopes.push(Scope(r));
return r;
}
}
AbstractConfigurable* t = scopes.top().ctx;
if (t == (AbstractConfigurable*)this) return NULL;
r = t->createContext(name);
if (r != NULL) {
scopes.push(Scope(r, r->getState()));
}
deep++;
return r;
}
bool Loader::closeContext() {
bool r = true;
if (scopes.empty()) return false;
AbstractConfigurable* t = scopes.top().ctx;
if (t != (AbstractConfigurable*)this) {
r = scopes.top().ctx->closeContext();
}
if (scopes.top().state != stTemplates) {
deep--;
}
scopes.pop();
dropTemplates(deep);
if (!scopes.empty()) {
scopes.top().ctx->setState(scopes.top().state);
}
return r;
}
bool Loader::setValue(const char* name, const char* value) {
if (scopes.empty()) return false;
if (getState() != stTemplate) {
if (strcmp(name, TEMPLATE_PROPERTY) == 0) {
return applyTemplate(value);
}
}
AbstractConfigurable* t = scopes.top().ctx;
if (t == (AbstractConfigurable*)this) return false;
return scopes.top().ctx->setValue(name, value);
}
bool Loader::applyTemplate(const char* name) {
for (TIter p = templates.begin(); p != templates.end(); ++p) {
if (p->second->isEqual(name)) {
p->second->setMagic(name);
return p->second->apply((AbstractConfigurable*)this);
}
}
return true;
}
}
Стек scopes используется для отслеживания вложенности секций описания. Объект находящийся верхушке стека является текущим обработчиком, и принимает все события, формируемые YAML-парсером при обработке описания. Можно видеть, что Loader также является наследником AbstractConfigurable. Это позволяет перехватывать обработку секций «templates» и атрибутов «template» в любом месте описания еще до того как событие будет обработано текущим обработчиком.
Реализация класса Template проста. Его задача — сохранить все события разбора в том порядке, в котором они получены, чтобы впоследствии воспроизвести их в другом месте описания, по требованию.
#ifndef _TEMPLATE_H_
#define _TEMPLATE_H_
#include <vector>
#include "AbstractConfigurable.h"
#include "VarHolder.h"
using namespace std;
namespace mf2 {
class Template: public AbstractConfigurable
, public VarHolder {
private:
enum {
itOpen = 0,
itClose = 1,
itSet = 2
};
struct Item {
Item(const char* nm = NULL, const char* vl = NULL): name(nm), value(vl) {}
Item(const Item& p): name(p.name), value(p.value) {}
StringValue name;
StringValue value;
int getType() const;
};
int deep;
Template* next;
int level;
vector<Item> items;
public:
Template(const char* name, Template* t, int l);
virtual AbstractConfigurable* createContext(const char* name);
virtual bool setValue(const char* name, const char* value);
virtual bool closeContext();
int getLevel() const {return level;}
Template* getNext() {return next;}
bool apply(AbstractConfigurable* ctx);
typedef vector<Item>::iterator IIter;
};
}
#endif // _TEMPLATE_H_
#include "Template.h"
#include "s3eTypes.h"
namespace mf2 {
Template::Template(const char* name, Template* t, int l): AbstractConfigurable(), VarHolder(NULL, name), next(t), level(l), items(), deep(0) {}
int Template::Item::getType() const {
if (name.isNull()) {
return itClose;
}
if (value.isNull()) {
return itOpen;
}
return itSet;
}
AbstractConfigurable* Template::createContext(const char* name) {
items.push_back(Item(name));
deep++;
return this;
}
bool Template::setValue(const char* name, const char* value) {
items.push_back(Item(name, value));
return true;
}
bool Template::closeContext() {
if (deep == 0) return true;
deep--;
items.push_back(Item());
return true;
}
bool Template::apply(AbstractConfigurable* ctx) {
for (IIter p = items.begin(); p != items.end(); ++p) {
switch (p->getType()) {
case itOpen:
if (ctx->createContext(p->name.getString()) == NULL) return false;
break;
case itClose:
if (!ctx->closeContext()) return false;
break;
case itSet: {
Value v(p->value.getString());
magicValue(v);
if (!ctx->setValue(p->name.getString(), v.getString())) return false;
}
break;
}
}
return true;
}
}
Теперь, можно еще раз взглянуть на иерархию классов и вспомнить, что Template наследуется не только от AbstractConfigurable, но и от VarHolder, являющегося частью Runtime-а. Это необходимо, чтобы обрабатывать «магические» определения следующего вида:
wt_N: { action: w_N,
name: wr_N,
template: lock,
x: 1,
template: y,
width: -1,
height: 30,
image: w_N
},
Заглавные буквы в имени шаблона рассматриваются как переменные. Если после определения этого шаблона будет выполнена попытка применения шаблона, например, с именем «wt_1», в определении шаблона будет сохранена переменная «N», имеющая значение 1. Все значения, загружаемые в шаблоне (независимо от уровня вложенности) будут проверяться на предмет наличия символа 'N'. В случае, если он обнаружен, он будет заменен на символ '1'.
Посмотрим, как это работает:
#ifndef _VAR_HOLDER_H_
#define _VAR_HOLDER_H_
#include <map>
#include "StringValue.h"
#include "Value.h"
using namespace std;
namespace mf2 {
class VarHolder {
private:
VarHolder* parent;
StringValue name;
map<StringValue, Value> values;
void addMagic();
int getMagicValue(char c);
protected:
void magicValue(Value& v);
public:
VarHolder(VarHolder* p, const char* nm = "");
void setMagic(const char* actualName);
bool isEqual(const char* actualName);
static bool isEmpty(const char* v);
template<class T>
void defVar(const char* name, T value);
template<class T>
void setVar(const char* name, T value);
bool isExists(const char* name);
const char* getString(const char* name);
int getInt(const char* name);
typedef map<StringValue, Value>::iterator VIter;
typedef pair<StringValue, Value> VPair;
};
}
#endif // _VAR_HOLDER_H_
#include "VarHolder.h"
#include <string.h>
namespace mf2 {
VarHolder::VarHolder(VarHolder* p, const char* nm): name(nm), parent(p), values() {
addMagic();
}
template<class T>
void VarHolder::defVar(const char* name, T value) {
VIter p = values.find(StringValue(name));
if (p != values.end()) {
p->second = Value(value);
} else {
values.insert(VPair(StringValue(name), Value(value)));
}
}
template<class T>
void VarHolder::setVar(const char* name, T value) {
VIter p = values.find(StringValue(name));
if (p != values.end()) {
p->second = Value(value);
return;
}
if (parent != NULL) {
parent->setVar(name, value);
}
}
bool VarHolder::isExists(const char* name) {
VIter p = values.find(StringValue(name));
if (p != values.end()) {
return true;
}
if (parent != NULL) {
return parent->isExists(name);
}
return false;
}
const char* VarHolder::getString(const char* name) {
VIter p = values.find(StringValue(name));
if (p != values.end()) {
return p->second.getString();
}
if (parent != NULL) {
return parent->getString(name);
}
return "";
}
int VarHolder::getInt(const char* name) {
VIter p = values.find(StringValue(name));
if (p != values.end()) {
return p->second.getInt();
}
if (parent != NULL) {
return parent->getInt(name);
}
return 0;
}
void VarHolder::addMagic() {
const char* s = name.getString();
for (int i = 0; i < (int)strlen(s); i++) {
if ((s[i] >= 'A')&&(s[i] <= 'Z')) {
defVar(StringValue(s[i]).getString(), 0);
}
}
}
int VarHolder::getMagicValue(char c) {
if ((c >= '0') && (c <= '9')) {
return (int)(c - '0');
}
if ((c >= 'a') && (c <= 'z')) {
return (int)(c - 'a' + 10);
}
return -1;
}
void VarHolder::setMagic(const char* actualName) {
const char* s = name.getString();
for (int i = 0; i < (int)strlen(s); i++) {
if (i >= (int)strlen(actualName)) break;
if ((s[i] >= 'A')&&(s[i] <= 'Z')) {
int v = getMagicValue(actualName[i]);
if (v >= 0) {
setVar(StringValue(s[i]).getString(), v);
}
}
}
}
bool VarHolder::isEqual(const char* actualName) {
const char* s = name.getString();
for (int i = 0; i < (int)strlen(s); i++) {
if (i >= (int)strlen(actualName)) return false;
if ((s[i] >= 'A')&&(s[i] <= 'Z')) continue;
if (s[i] != actualName[i]) return false;
}
return true;
}
bool VarHolder::isEmpty(const char* v) {
if (v == NULL) return true;
if (v[0] == 0) return true;
return false;
}
void VarHolder::magicValue(Value& v) {
const char* value = v.getString();
for (int i = 0; i < (int)strlen(value); i++) {
if ((value[i] >= 'A')&&(value[i] <= 'Z')) {
StringValue name(value[i]);
if (isExists(name.getString())) {
const char* vl = getString(name.getString());
if (!isEmpty(vl)) {
v.replace(i, vl[0]);
value = v.getString();
}
}
}
}
}
}
Все очень просто. При сохранении шаблона, его имя анализируется методом addMagic, формирующим найденные переменные, а перед его применением оно сравнивается с фактическим именем методом setMagic, присваивающим этим переменным значения. Метод magicValue используется для преобразования любого значения, загружаемого при выполнении шаблона, независимо от уровня вложенности (это значение, например, может находиться в другом шаблоне, применяемом локально).
Разбор JSON-описания инициируется ScopeManager-ом (также являющимся частью Runtime-а). Его задачей является ведение списка файлов, подлежащих загрузке, а также разбор корневых секций описания. Вот как это выглядит:
#ifndef _SCOPE_MANAGER_H_
#define _SCOPE_MANAGER_H_
#include <map>
#include <queue>
#include "AbstractConfigurable.h"
#include "ScopeDef.h"
using namespace std;
#define MAIN_CONFIG "main.json"
#define DISPLAY_SCOPE "display"
#define SCOPES_SCOPE "scopes"
#define LOAD_PROPERTY "load"
namespace mf2 {
class ScopeManager: public AbstractConfigurable {
private:
enum {
stTop = 0,
stScopes = 1
};
queue<const char*>* files;
map<StringValue, ScopeDef*>* scopes;
bool load(const char* name);
void deepIncrement();
void deepDecrement();
virtual AbstractConfigurable* createContext(const char* name);
virtual bool setValue(const char* name, const char* value);
public:
bool init();
void release();
typedef map<StringValue, ScopeDef*>::iterator SIter;
typedef pair<StringValue, ScopeDef*> SPair;
friend class AbstractConfigurable;
};
extern ScopeManager sm;
}
#endif // _SCOPE_MANAGER_H_
#include "ScopeManager.h"
#include "ConfigLoader.h"
#include "StringPool.h"
#include "Desktop.h"
namespace mf2 {
ScopeManager sm;
bool ScopeManager::init() {
setState(stTop);
desktop.init();
pool.init();
files = new queue<const char*>();
scopes = new map<StringValue, ScopeDef*>();
return load(MAIN_CONFIG);
}
void ScopeManager::release() {
for (SIter p = scopes->begin(); p != scopes->end(); ++p) {
delete p->second;
}
delete scopes;
delete files;
pool.release();
desktop.release();
}
bool ScopeManager::load(const char* name) {
Loader loader;
setState(stTop);
files->push(pool.getString(name));
while (!files->empty()) {
const char* fn = files->front();
files->pop();
if (!loader.load(fn, (AbstractConfigurable*)this)) return false;
pool.freeString(fn);
}
return true;
}
AbstractConfigurable* ScopeManager::createContext(const char* name) {
if (getState() == stTop) {
if (strcmp(name, DISPLAY_SCOPE) == 0) {
return &desktop;
}
if (strcmp(name, SCOPES_SCOPE) == 0) {
setState(stScopes);
return this;
}
}
if (getState() == stScopes) {
ScopeDef* r = new ScopeDef;
scopes->insert(SPair(StringValue(name), r));
return r;
}
return (AbstractConfigurable*)this;
}
bool ScopeManager::setValue(const char* name, const char* value) {
if (getState() == stTop) {
if (strcmp(name, LOAD_PROPERTY) == 0) {
files->push(pool.getString(value));
return true;
}
}
return false;
}
}
Здесь можно видеть, каким образом разбор секции «display» делегируется объекту display, а также, как создаются и сохраняются в коллекции именованные описания ScopeDef. Реализация классов ScopeDef, RegionDef и VarDef тривиальна (поскольку их можно посмотреть в репозитории, я не стану приводить их здесь), а класс Display мы будем рассматривать позднее, когда займемся реализацией Runtime.
Осталось создать тестовый проект и убедиться, что все загружается туда, куда нужно и при этом не разрушается память:
#include "ScopeManager.h"
using namespace mf2;
void init() {
sm.init();
}
void release() {
sm.release();
}
int main() {
init();
release();
return 0;
}
На этом, на сегодня, все. В следующих статьях, я рассмотрю разработку Runtime.
Автор: GlukKazan