Все записи автора Brainstream

Знакомство с Boost.Spirit.Qi

Столкнувшись с необходимостью написания парсера для небольшого встроенного DSL, я решил использовать Boost.Spirit. Но оказалось, что использовать библиотеку человеку, плохо знакомому с boost, весьма не просто. В этой статье я попытаюсь познакомить Вас с основными аспектами работы с Boost.Spirit, чтобы помочь быстрее начать пользоваться этой замечательной библиотекой.

Библиотека Boost.Spirit состоит из нескольких частей: Classic, Qi, Lex и Karma. Classic -- это старая реализация (1.x) Boost.Spirit, оставленная для совместимости. Lex -- это токенайзер, позволяющий разбить входящий текст на токены. Karma служит для форматирования аутпута. В этой же статье речь пойдёт о Qi -- второй версии парсера Boost.Spirit.

Boost.Spirit.Qi использует расширенную форму Бэкуса-Наура (EBNF), но, в отличие от yacc, не генерирует код парсера, а позволяет писать EBNF прямо в коде.

Для того, чтобы познакомиться с библиотекой, нам понадобится простой формальный язык. Пусть это будет арифметическое выражение со скобками и знаками "+" и "-", например:
1
(1)
1 - 2
1 - 2 + 3
(1 + 2)
3 + (1 + 2)
(1 + 2) + 3
(1 + 2 + 3) + 4
(1 + 2) + (3 + 4)
(1 + (2 + 3)) + 4 + (5 + 6)
(1 + (2 + 3)) + (4 + 5) + 6
1 + 2 + (3 - 4 + (5 + 6 - (7 - 8 + 9) - 10) - 11 + 12) + 13 + 14
Этого примера достаточно для понимания того, что нужно делать для написания собственного парсера. Вместе с этим, этот пример достаточно прост, чтобы не загромождать эту статью кодом.

Первое и самое сложное, что необходимо для создания парсера -- продумать EBNF языка. Для представленного выше языка EBNF будет следующей:
Оператор ::= "+" | "-"
Операнд ::= число | <Выражение в скобках>
<Выражение в скобках> ::= "(" Выражение ")"
<Правая часть выражения> ::= Оператор Операнд
Выражение ::= Операнд { <Правая часть выражения> }
Далее нужно описать структуры данных для хранения данных каждого правила. Для того, чтобы одно и то же поле могло хранить данные разных типов (что необходимо, например, для реализации правила Операнд), следует использовать библиотеку Boost.Variant, которая поддерживается парсерами Spirit. Эта же библиотека позволяет разрешить проблему с рекурсивным вхождением правил друг в друга.
Итак, давайте посмотрим на заголовочный файл нашего парсера.
#include <memory>
#include <string>
#include <ostream>
#include <vector>
#include <boost/variant.hpp>

namespace Parser {

enum class Operator
{
plus,
minus
};

typedef int Number;

struct Expression;

typedef boost::variant<Number, boost::recursive_wrapper<Expression>> Operand;

struct RightExpression
{
Operator operator_;
Operand operand;
};

struct Expression
{
Operand operand;
std::vector<RightExpression> right;
};

std::shared_ptr<Expression> parse(const std::string & _input);

} // namespace Parser

std::ostream & operator << (std::ostream & _stream, const Parser::Operator & _operator);
std::ostream & operator << (std::ostream & _stream, const Parser::Operand & _operand);
std::ostream & operator << (std::ostream & _stream, const Parser::Expression & _expression);
Как видно, структуры данных в точности повторяют описания в EBNF. Структура для правила <Выражение в скобках> отсутствует, так как это правило идентично правилу Выражение, за исключением скобок, которые не являются данными. Повторяющиеся вхождения правил может быть записано с помощью стандартного вектора, что мы и сделали для правила Выражение при записи вхождения <Правая часть выражения>.
Наибольший интерес вызывает строка
typedef boost::variant<Number, boost::recursive_wrapper<Expression>> Operand;
Здесь применяется библиотека Boost.Variant. Это выражение декларирует тип Operand, который может хранить либо число, либо Expression. Класс boost::recursive_wrapper позволяет избежать ошибки компиляции из-за использования незавершённых типов. Такая ошибка происходит, потому что для типа Operand требуется тип Expression, а для типа Expression требуется тип Operand.

Описав все необходимые типы, мы сталкиваемся с новой проблемой -- Boost.Spirit не умеет с ними работать. Чтобы Boost.Spirit научился работать с нашими типами, необходимо сгенерировать дополнительную информацию с помощью макроса BOOST_FUSION_ADAPT_STRUCT, входящего в библиотеку Boost.Fusion.
BOOST_FUSION_ADAPT_STRUCT(
RightExpression,
(Operator, operator_)
(Operand, operand)
)

BOOST_FUSION_ADAPT_STRUCT(
Expression,
(Operand, operand)
(std::vector<RightExpression>, right)
)
Теперь мы можем приступить к написанию парсера.
namespace qi = boost::spirit::qi;

class OperatorSymbol : public qi::symbols<char, Operator>
{
public:
OperatorSymbol()
{
add
("+", Operator::plus)
("-", Operator::minus);
}
};

class ExpressionParser :
public qi::grammar<typename std::string::const_iterator, Expression(), qi::space_type>
{
private:
template<typename T>
using Rule = qi::rule<typename std::string::const_iterator, T, qi::space_type>;

public:
ExpressionParser();

private:
OperatorSymbol m_operator;
Rule<Operand()> m_operand;
Rule<RightExpression()> m_right_exp;
Rule<Expression()> m_bracketed_expression;
Rule<Expression()> m_expression;
};

ExpressionParser::ExpressionParser() :
ExpressionParser::base_type(m_expression)
{
m_operand %= qi::int_ | m_bracketed_expression;
m_right_exp %= m_operator >> m_operand;
m_bracketed_expression %= '(' >> m_expression >> ')';
m_expression %= m_operand >> *m_right_exp;
}
Здесь класс OperatorSymbol описывает правила преобразования символа в элемент перечисления. Подобные классы необходимо наследовать от класса boost::spirit::qi::symbols с указанием в шаблоне типа символа и результирующего типа. Экземпляры наследников класса symbols можно использовать в качестве правила. В данном случае, экземпляр класса OperatorSymbol используется для реализации правила Оператор.
Далее, класс парсера, ExpressionParser, наследуется от класса boost::spirit::qi::grammar. Шаблон этого класса первым параметром принимает тип итератора входящих данных. Второй, третий и четвёртый параметры опциональны, но почти всегда в них нужно передавать сигнатуру создания результирующей структуры данных (обычно это сигнатура конструктора по умолчанию) и тип символов, которые будут пропускаться парсером (обычно это пробелы). Более подробно с параметрами шаблона grammar можно ознакомиться в документации.
Для построения EBNF используются стандартные парсеры для чисел, символов и прочего, коих очень много, поэтому я отсылаю Вас за ними в документацию. В данном примере мы можем видеть использование персера boost::spirit::qi::int_. Необходимо отметить, что некоторые  парсеры, такие как char_ лежат внутри неймспейсов с именем, совпадающим с именем кодировки символов, например boost::spirit::qi::ascii::char_. Кроме стандартных парсеров, можно создавать свои правила, используя класс boost::spirit::qi::rule. Шаблон этого класса совпадает с шаблоном grammar.
Конструктор нашего ExpressionParser должен передать в конструктор базового класса правило, которое будет использовано для разбора всего входящего текста. Это правило должно иметь тип, совпадающий с типом, переданным в шаблон grammar. Базовый конструктор проще всего вызывать через typedef base_type, так как имя базового класса весьма длинное.
EBNF записывается в достаточно простом виде. Естественно, я не буду перечислять здесь все доступные операторы, это займёт много места и времени. Вместо этого снова отсылаю Вас в документацию.

Пришло время применить наш парсер.
std::shared_ptr<Expression> Parser::parse(const std::string & _input)
{
ExpressionParser parser;
Expression * expression = new Expression();
std::shared_ptr<Expression> result(expression);
if(qi::phrase_parse(_input.begin(), _input.end(), parser, qi::space, *expression))
return result;
return nullptr;
}
Есть несколько способов применения парсера, наиболее распространённым из них является вызов функции boost::spirit::qi::phrase_parse. Функция возвращает true, если строка распаршена удачно. Для того, чтобы получить место падения парсера, отсылаю Вас к статье на официальном сайте.

Для тестирования парсера нам понадобится выводить содержимое структур данных в консоль. Для этого реализуем операторы <<.
class DefaultPrintVisitor : public boost::static_visitor<>
{
public:
explicit DefaultPrintVisitor(std::ostream & _stream) :
mr_stream(_stream)
{
}

template<typename T>
void operator ()(const T & _value)
{
mr_stream << _value;
}

private:
std::ostream & mr_stream;
};

std::ostream & operator << (std::ostream & _stream, const Operator & _operator)
{
switch(_operator)
{
case Operator::plus:
_stream << "+";
break;
case Operator::minus:
_stream << "-";
break;
}
return _stream;
}

std::ostream & operator << (std::ostream & _stream, const Operand & _operand)
{
DefaultPrintVisitor visitor(_stream);
boost::apply_visitor(visitor, _operand);
return _stream;
}

std::ostream & operator << (std::ostream & _stream, const Expression & _expression)
{
_stream << '(' << _expression.operand;
for(const RightExpression & rexp : _expression.right)
_stream << ' ' << rexp.operator_ << ' ' << rexp.operand;
_stream << ')';
return _stream;
}
Для того, чтобы работать с boost::variant библиотекой предлагается использовать паттерн visitor. Для этого Вам нужно унаследовать Ваш Visitor от boost::static_visitor<>, и определить оператор () для каждого из типов, входящих в шаблон boost::variant. Экземпляр этого класса нужно передать функции boost::apply_visitor вместе со ссылкой на экземпляр boost::variant, что и продемонстрировано в operator << для Operand.

Протестируем то, что получилось на нескольких примерах. Уберём в некоторых местах пробелы и наставим побольше скобок.

#include <iostream>
#include "Parser.h"

using namespace Parser;

int main()
{
const std::string sources[] = {
"1",
"(1)",
"1 - 2",
"1 - 2 + 3",
"1-2+3",
"(1 + 2)",
"3 + (1 + 2)",
"(1 + 2) + 3",
"(1-2)+3",
"(1 + 2 + 3) + 4",
"(1 + 2) + (3 + 4)",
"(1 + (2 + 3)) + 4 + (5 + 6)",
"(1 + (2 + 3)) + (4 + 5) + 6",
"1 + 2 + (3 - 4 + (5 + 6 - (7 - 8 + 9) - 10) - 11 + 12) + 13 + 14"
};
for(const std::string & src : sources)
{
std::cout << "Parsing string: " << src << ": ";
std::shared_ptr<Expression> expression = parse(src);
if(expression)
std::cout << "succeeded\nResult: " << *expression;
else
std::cout << "failed";
std::cout << std::endl << std::endl;
}
return 0;
}
Parsing string: 1: succeeded
Result: (1)

Parsing string: (1): succeeded
Result: ((1))

Parsing string: 1 - 2: succeeded
Result: (1 - 2)

Parsing string: 1 - 2 + 3: succeeded
Result: (1 - 2 + 3)

Parsing string: 1-2+3: succeeded
Result: (1 - 2 + 3)

Parsing string: (1 + 2): succeeded
Result: ((1 + 2))

Parsing string: 3 + (1 + 2): succeeded
Result: (3 + (1 + 2))

Parsing string: (1 + 2) + 3: succeeded
Result: ((1 + 2) + 3)

Parsing string: (1-2)+3: succeeded
Result: ((1 - 2) + 3)

Parsing string: (1 + 2 + 3) + 4: succeeded
Result: ((1 + 2 + 3) + 4)

Parsing string: (1 + 2) + (3 + 4): succeeded
Result: ((1 + 2) + (3 + 4))

Parsing string: (1 + (2 + 3)) + 4 + (5 + 6): succeeded
Result: ((1 + (2 + 3)) + 4 + (5 + 6))

Parsing string: (1 + (2 + 3)) + (4 + 5) + 6: succeeded
Result: ((1 + (2 + 3)) + (4 + 5) + 6)

Parsing string: 1 + 2 + (3 - 4 + (5 + 6 - (7 - 8 + 9) - 10) - 11 + 12) + 13 + 14: succeeded
Result: (1 + 2 + (3 - 4 + (5 + 6 - (7 - 8 + 9) - 10) - 11 + 12) + 13 + 14)

Все исходники примера лежат тут.

Пишем монитор pulseaudio для lxpanel

С момента написания статьи об xmonad кое-что поменялось и я перешёл на использование openbox. Поменялись и панели, теперь я использую lxpanel, который разрабатывается для lxde, легковесного рабочего стола, основанного на openbox. Отображаемые lxpanel элементы являются плагинами, динамически загружаемыми библиотеками. Сегодня мы напишем свой плагин для lxpanel.
Так как в стандартной поставке lxpanel виджет для регулировки громкости не очень хорошо (скорее очень нехорошо) отображает текущий уровень громкости, то я решил написать свой плагин, который исправит ситуацию.
Эта статья будет состоять из двух частей. Первая будет посвящена работе с pulseaudio (читается [pʌlsˈɔːdɪəu] пaлсодыо), а вторая -- lxpanel.


На скриншоте выше результат можно разглядеть слева.
Писать мы будем на C. Да, именно на C, так как заголовочный файл lxpanel является ярким представителем того, как можно писать на C несовместимый с C++ код.

Внимание, весь код, приведённый в этой статье распространяется по лицензии GNU GPL версии 3 и выше!

Монитор Pulseaudio

Начиная работать с pulseaudio натыкаешься на, мягко говоря, скудную документацию, основу которой составляет сгенерированная doxygen`ом. Из документации мы узнаём, что у pulseaudio есть два типа API: simple и asynchronous. Беглый взгляд на simple API позволяет понять, что придётся использовать асинхронный API.
Основным элементом для работы с асинхронным API pulseaudio является контекст, представленный типом pa_context. Контекст представляет собой мост между приложением и демоном pulseaudio. Чтобы контекст работал, ему нужен mainloop. Pulseaudio представляет возможность использовать три варианта реализации основного цикла:
Из перечисленных вариантов нам больше всего подходит последний, так как lxpanel написана на GTK+ 2 и уже имеет запущенный GMainLoop. Остальные варианты приведут либо к зависанию lxpanel, либо к сложной и запутанной архитектуре.
Так как на машине может быть несколько звуковых устройств, то их надо как-то различать. Pulseaudio использует для этого понятие sink (не знаю, как правильно перевести). Sink`и имеют свой идентификатор или индекс, который является порядковым номером, начиная с нуля.
Итак, на основе полученной информации мы уже можем накидать интерфейс, который мы будем использовать для получения информации от pulseaudio.
#include <stdint.h>

#define PAMON_UNUSED(var) (void)var

typedef struct Pamon Pamon;

typedef enum
{
PAMON_ERROR_CONNECTION_FAILED,
PAMON_ERROR_INVALID_SINK
} PamonError;

typedef void (* PamonVolumeChangedCallback)(uint32_t current_volume_percents, void * userdata);
typedef void (* PamonErrorCallback)(PamonError error, void * userdata);

typedef struct
{
uint32_t sink_id;
PamonVolumeChangedCallback volume_callback;
PamonErrorCallback error_callback;
void * userdata;
} PamonInitData;

Pamon * pamonInit(PamonInitData * init_data);

void pamonSetSinkId(Pamon * pamon, uint32_t id);

int pamonStart(Pamon * pamon);

void pamonClose(Pamon * pamon);

const char * pamonTranslateError(PamonError error);
Немного поясню. Тип PamonError используется для сообщения об ошибке. Значение PAMON_ERROR_CONNECTION_FAILED говорит о том, что соединиться с демоном pulseaudio не удалось, а PAMON_ERROR_INVALID_SINK сообщает о неверном идентификаторе sink`а. Функция pamonTranslateError переводит идентификатор ошибки в понятный человеку вид, в строку.
Фунции pamonInit и pamonClose соответственно инициализируют и закрывают соединение с демоном pulseaudio. Для инициализации соединения в функцию pamonInit передаётся структура PamonInitData содержащая идентификатор sink`а и колбеки для уведомления о смене громкости и об ошибке. Кроме того структура  PamonInitData содержит поле userdata, которое будет передано в колбеки.
Назначение функции pamonSetSinkId должно быть ясно без пояснений.
Используя этот интерфейс можно написать консольное приложение, которое будет выводить текущий уровень громкости.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <glib.h>
#include "pamon.h"

#define APP_NAME "pamon"
#define DEFAULT_FORMAT "%i%%"

typedef struct
{
char * format;
uint32_t sink;
int print_help;
} Options;

static void pamonVolumeChanged(uint32_t current_volume, void * userdata);
static void pamonError(PamonError error, void * userdata);
static void onSignal(int signum);
static void parseOpts(int argc, char ** argv, Options * result);
static void printUsage();

static Pamon * pamon = NULL;
static GMainLoop * main_loop = NULL;
static char * format = NULL;


int main(int argc, char ** argv)
{
struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = onSignal;
sigaction(SIGINT, &action, NULL);
PamonInitData pamon_init_data;
memset(&pamon_init_data, 0, sizeof(PamonInitData));
{
Options opts;
memset(&opts, 0, sizeof(Options));
parseOpts(argc, argv, &opts);
if(opts.print_help)
{
printUsage();
free(opts.format);
return EXIT_SUCCESS;
}
pamon_init_data.sink_id = opts.sink;
format = opts.format;
}
pamon_init_data.volume_callback = pamonVolumeChanged;
pamon_init_data.error_callback = pamonError;
main_loop = g_main_loop_new(NULL, FALSE);
pamon = pamonInit(&pamon_init_data);
g_main_loop_run(main_loop);
return EXIT_SUCCESS;
}

void onSignal(int signum)
{
pamonClose(pamon);
g_main_loop_quit(main_loop);
g_main_loop_unref(main_loop);
free(format);
printf("\nquit\n");
exit(EXIT_SUCCESS);
}

void pamonVolumeChanged(uint32_t current_volume, void * userdata)
{
PAMON_UNUSED(userdata);
printf(format ? format : DEFAULT_FORMAT, current_volume);
printf("\n");
fflush(stdout);
}

void pamonError(PamonError error, void * userdata)
{
PAMON_UNUSED(userdata);
fprintf(stderr, "Error: %s\n", pamonTranslateError(error));
}

void parseOpts(int argc, char ** argv, Options * result)
{
for(;;)
{
switch(getopt(argc, argv, "f:s:"))
{
case -1:
return;
case '?':
result->print_help = 1;
break;
case 'f':
result->format = (char *)malloc(strlen(optarg) + 1);
strcpy(result->format, optarg);
break;
case 's':
sscanf(optarg, "%i", &result->sink);
break;
default:
break;
}
}
}

void printUsage()
{
printf(
"Usage: %s [-s sink] [-f format]\n"
" sink - pulseaudio sink id\n"
" format - print format. Use %%i to print current volume value. Use %%%% to print the %% mark\n"
"\n"
"Use Ctrl+C to exit program\n",
APP_NAME);
fflush(stdout);
}
Здесь мы получаем из опций приложения идентификатор sink`а и формат сообщения. Создаём основной цикл GLib и запускам pamon, передав функции pamonVolumeChanged и pamonError в качестве колбеков. Программа завершается по нажатию Ctrl+C. Обработчик сигнала SIGINT позволит освободить занятые ресурсы перед выходом, хотя это и не обязательно.

Перейдём к самому интересному, к реализации интерфейса взаимодействия с pulseaudio.
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <pulse/pulseaudio.h>
#include <pulse/glib-mainloop.h>
#include "pamon.h"

struct Pamon
{
pa_context * pulse_context;
pa_glib_mainloop * pulse_glib_mainloop;
PamonVolumeChangedCallback volume_callback;
PamonErrorCallback error_callback;
int sink_id;
void * userdata;
};

Pamon * pamonInit(PamonInitData * init_data)
{
Pamon * pamon = (Pamon *)malloc(sizeof(Pamon));
memset(pamon, 0, sizeof(Pamon));
if(init_data)
{
pamon->userdata = init_data->userdata;
pamon->volume_callback = init_data->volume_callback;
pamon->error_callback = init_data->error_callback;
pamon->sink_id = init_data->sink_id;
}
pamon->pulse_glib_mainloop = pa_glib_mainloop_new(NULL);
pa_mainloop_api * api = pa_glib_mainloop_get_api(pamon->pulse_glib_mainloop);
pamon->pulse_context = pa_context_new(api, NULL);
pa_context_set_state_callback(pamon->pulse_context,
(pa_context_notify_cb_t)pulseContextStateCallback, pamon);
pa_context_connect(pamon->pulse_context, NULL, 0, NULL);
return pamon;
}
Функция инициализации создаёт и заполняет экземпляр структкры Pamon. Затем создаётся основной цикл из дефолтного контекста GLib и, после получения API основного цикла, инициализируется контекст. pa_mainloop_api служит абстрактным интерфейсом для работы со всеми тремя типами основных циклов. Так как мы используем асинхронные API и соединение с демоном pulseaudio происходит в отдельном потоке, функция pa_context_connect может сообщить о своём результате только в функцию обратного вызова, которую мы и устанавливаем с помощью pa_context_set_state_callback. При установке колбеков мы можем передавать пользовательские данные, которые будут отданы колбеку. Здесь и далее мы будем использовать указатель на структуру Pamon, хранящую всю служебную информацию.
void pulseContextStateCallback(pa_context * context, Pamon * pamon)
{
switch(pa_context_get_state(context))
{
case PA_CONTEXT_READY:
pa_context_set_subscribe_callback(context, (pa_context_subscribe_cb_t)pulseContextCallback, pamon);
pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK, NULL, NULL);
performSinkHandler(context, pamon);
break;
case PA_CONTEXT_FAILED:
performError(pamon, PAMON_ERROR_CONNECTION_FAILED);
break;
default:
break;
}
}
Здесь мы подписываемся на события контекста с помощью функции pa_context_subscribe, устнавив колбек вызовом pa_context_set_subscribe_callback. Так как нам интересны только события от sink`ов, то передаём маску PA_SUBSCRIPTION_MASK_SINK для фильтрации того, что нам будет приходить в колбек.
Если произошла ошибка, о ней сообщаем вызовом performError, который, по сути, зовёт колбек, переданный при инициализации.
void performError(Pamon * pamon, PamonError error)
{
if(pamon->error_callback)
pamon->error_callback(error, pamon->userdata);
}
Кроме подписки на события, мы форсируем получение значения громкости для изначальной инициализации подписчика.
void performSinkHandler(pa_context * context, Pamon * pamon)
{
pa_operation * operation = pa_context_get_sink_info_by_index(context, pamon->sink_id,
(pa_sink_info_cb_t)pulseSinkInfoCallback, pamon);
pa_operation_unref(operation);
}
Как видно, функция performSinkHandler выполняет только одно действие -- просит информацию о sink`е по его идентификатору вызовом функции pa_context_get_sink_info_by_index. Так как информация о ходе выполнения асинхронной операции нам не интересна, то сразу же отпускаем ссылку на экземпляр pa_operation вызвав pa_operation_unref. Итак, информация о sink`е приходит в функцию pulseSinkInfoCallback.
void pulseSinkInfoCallback(pa_context * context, const pa_sink_info * info, int eol, Pamon * pamon)
{
if(eol == -1)
{
performError(pamon, PAMON_ERROR_INVALID_SINK);
}
if(info && pamon && pamon->volume_callback)
{
uint32_t volume = info->mute || !info->volume.channels ? 0 : info->volume.values[0];
uint32_t percents = (uint32_t)round((double)(volume * 100) / PA_VOLUME_NORM);
pamon->volume_callback(percents, pamon->userdata);
}
}
Я не нашёл документации о параметре eol, но методом научного тыка выяснил, что при ошибке этот флаг принимает значение -1.
Структура pa_sink_info содержит всю необходимую нам информацию. Уровень громкости тут содержится не в процентах, а в довольно больших числах. Кроме того, уровень громкости можно получить для каждого канала отдельно. Поле channels содержит количество доступных каналов, а массив values, состоящий из 32-ух целочисленных значений имеет инициализированными ровно channels первых элементов. Здесь я схалтурил и взял первое попавшееся значение. Карту соответствия индексов массива с реальными каналами можно посмотреть в поле channel_map структуры pa_sink_info.
Высчитать текущий уровень громкости в процентах помогает константа PA_VOLUME_NORM, которая содержит значение громкости при 100%, 65536.

На этом наша работа с pulseaudio заканчивается. Осталось только корректно завершить работу.
void pamonClose(Pamon * pamon)
{
if(!pamon) return;
pa_context_disconnect(pamon->pulse_context);
pa_context_unref(pamon->pulse_context);
pa_glib_mainloop_free(pamon->pulse_glib_mainloop);
free(pamon);
}
Для этого отсоединяемся от демона pulseaudio c помощью вызова pa_context_disconnect, отпускаем ссылку на контекст с помощью pa_context_unref и высвобождаем память, занятую основным циклом используя pa_glib_mainloop_free. Естественно не забываем и о нашей структуре, хранящей служебную информацию. Следует обратить внимание, что для завершения работы с другими типами mainloop`ов нужно выполнять действия, соответствующие выбранному типу.

За бортом остались две функции: pamonSetSinkId и pamonTranslateError. Их назначение понятно, а код прост, поэтому приведу его без комментариев.
#define ERSTR_UNKNOWN "Unknown error"
#define ERSTR_INVALID_SINK "Invalid sink"
#define ERSTR_CONNECTION_FAILED "Unable to connect to the pulseaudio daemon"

void pamonSetSinkId(Pamon * pamon, uint32_t id)
{
if(!pamon || pamon->sink_id == id)
return;
pamon->sink_id = id;
performSinkHandler(pamon->pulse_context, pamon);
}

const char * pamonTranslateError(PamonError error)
{
switch(error)
{
case PAMON_ERROR_CONNECTION_FAILED:
return ERSTR_CONNECTION_FAILED;
case PAMON_ERROR_INVALID_SINK:
return ERSTR_INVALID_SINK;
default:
return ERSTR_UNKNOWN;
}
}

lxpanel

Полученное приложение уже можно использовать, например, в conky или dzen2. Так
выглядит dzen2, запущенный командой
$ ./pamon -f " Current volume: %i%%" | dzen2 -bg "#505050" -fg white -ta left
Но наша цель -- встроить монитор в lxpanel, поэтому создаём ещё один файл, lxpamon.c, который будем компилировать в динамическую библиотеку.
Первое, что нам надо сделать -- это подключить необходимые заголовочные файлы.
#include <stdlib.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <lxpanel/plugin.h>
#include "pamon.h"
Файл lxpanel/plugin.h и есть тот самый файл, который не способен переварить компилятор C++.
Документации по написанию плагинов для lxpanel ещё меньше, чем документации по pulseaudio, а точнее -- ровно одна страничка. Даже зачитав её до дыр, Вы не сможете написать полноценный плагин. Для того, чтобы написать свой, мне пришлось посмотреть в исходинки плагинов, поставляемых с lxpanel. Я скачал исходники из репозитория debian по этой ссылке. Файл src/plugins/dclock.c, содержащий исходники плагина с часами, оказался наиболее компактным и полезным.
Итак, для того, чтобы встроить плагин в панель, мы должны собрать библиотеку и положить её в  /usr/lib/lxpanel/plugins/ (или в  /usr/lib/x86_64-linux-gnu/lxpanel/plugins/ для мультиархитектурного debian`а). Есть одна особенность, имя файла не должно содержать префикс lib и иметь имя <имя_плагина>.so. В нашем случае файл будет называться lxpamon.so.
Библиотека с плагином должна экспозить экземпляр структуры PluginClass (к сожалению, ссылок мне давать не на что). Но не просто экспозить, а экспозить с определённым именем. Имя должно иметь вид <имя_плагина>_plugin_class. То есть, наш экземпляр будет иметь имя lxpamon_plugin_class.
PluginClass lxpamon_plugin_class = {
PLUGINCLASS_VERSIONING,
type: "lxpamon",
name: N_("Pulseaudio volume monitor"),
version: "1.0",
description: N_("Pulseaudio volume monitor plugin for LXPanel"),
one_per_system: FALSE,
expand_available: FALSE,
constructor: lxpamonConstructor,
destructor: lxpamonDestructor,
config: lxpamonConfigure,
save: lxpamonSaveConfig,
panel_configuration_changed: lxpamonPanelConfigurationChanged
};
Структура PluginClass описывает наш плагин. Макрос PLUGINCLASS_VERSIONING заполняет поля для определения версии структуры. Поле type должно содержать имя нашего плагина. Поля version и description вполне понятны. Флаг one_per_system позволяет ограничит количество активных экземпляров плагина одной штукой, а флаг expand_available разрешает растягивать виджет на всю свободную область панели (как tasklist или spacer). Макрос N_ объявлен в GLib и предназначен для интернационализации.
Оставшиеся поля контролируют жизненный цикл плагина, это следующие процедуры обратного вызова:
  • constructor -- инициализирует плагин;
  • destructor -- вызывается перед выгрузкой плагина;
  • config -- колбек, который будет зваться для редактирования настроек (например, при нажатии на кнопку Edit в настройках элементов панели);
  • save -- зовётся для сохранения настроек в конфиг;
  • panel_configuration_changed -- колбек, сообщающий, что конфигурация самой панели изменилась.
Для того, чтобы хранить текущее состояние плагина, будем использовать структуру PluginData.
typedef struct
{
Pamon * pamon;
GtkWidget * label;
uint32_t current_volume;
int sink;
char * format;
int bold_font;
} PluginData;
И сразу же приведу код конструктора.
int lxpamonConstructor(Plugin * plugin, char ** config)
{
GtkWidget * label = gtk_label_new(NULL);
plugin->pwid = gtk_event_box_new();
gtk_container_add(GTK_CONTAINER(plugin->pwid), GTK_WIDGET(label));
g_signal_connect(G_OBJECT(plugin->pwid), "button_press_event",
G_CALLBACK(plugin_button_press_event), plugin);
gtk_widget_show_all(plugin->pwid);
PluginData * plug_data = g_new0(PluginData, 1);
plug_data->label = label;
lxpamonInitConfig(config, plug_data);
plugin->priv = plug_data;
PamonInitData pamon_init_data;
memset(&pamon_init_data, 0, sizeof(PamonInitData));
pamon_init_data.volume_callback = (PamonVolumeChangedCallback)pamonVolumeChanged;
pamon_init_data.error_callback = (PamonErrorCallback)pamonError;
pamon_init_data.userdata = plugin;
pamon_init_data.sink_id = plug_data->sink;
Pamon * pamon = pamonInit(&pamon_init_data);
plug_data->pamon = pamon;
return TRUE;
}
Здесь создаются GTK+ контейнер и label, который кладётся в контейнер. Вообще говоря, Вы может создавать любые виджеты. Осонвное, что Вы должны сделать -- положить указатель на основной виджет в поле pwid структуры Plugin, которая передаётся на вход конструктору. Вообще, указатель на структуру Plugin будет передаваться во все колбеки. Эта структура содержит кое-какие полезные поля, с которыми мы познакомимся по ходу дела. Поле priv предназначено для хранения пользовательских данных, поэтому положим туда указатель на объект структуры PluginData.
Вторым параметром в конструктор передаётся конфиг, который мы читаем в функции lxpamonInitConfig. Сохраняется конфиг, как уже было сказано, функцией lxpamonSaveConfig.
#define DEFAULT_FORMAT "%i%%"
#define CONFIG_FORMAT "Format"
#define CONFIG_SINK "Sink"
#define CONFIG_BOLD "BoldFont"

void lxpamonInitConfig(char ** src, PluginData * plug_data)
{
line str;
str.len = 256;
while(lxpanel_get_line(src, &str))
{
if(strcmp(CONFIG_FORMAT, str.t[0]) == 0)
plug_data->format = g_strdup(str.t[1]);
else if(strcmp(CONFIG_SINK, str.t[0]) == 0)
sscanf(str.t[1], "%i", &plug_data->sink);
else if(strcmp(CONFIG_BOLD, str.t[0]) == 0)
plug_data->bold_font = str2num(bool_pair, str.t[1], 0);
}
if(!plug_data->format)
plug_data->format = g_strdup(DEFAULT_FORMAT);
}

void lxpamonSaveConfig(Plugin * plugin, FILE * file)
{
PluginData * plug_data = (PluginData *)plugin->priv;
lxpanel_put_str(file, CONFIG_FORMAT, plug_data->format);
lxpanel_put_int(file, CONFIG_SINK, plug_data->sink);
lxpanel_put_bool(file, CONFIG_BOLD, plug_data->bold_font);
}
Для записи данных в конфиг, используются макросы lxpanel_put_str, lxpanel_put_bool и lxpanel_put_int, кторые принемают файл, имя параметра и значение. Наш конфиг будет выглядеть примерно так
Plugin {
type = lxpamon
Config {
Format=%i%%
Sink=0
BoldFont=1
}
}
Конфиги лежат в ~/.config/lxpanel/default/panels/<имя_панели>. Для последовательного чтения записей из конфига следует использовать функцию lxpanel_get_line. Она записывает в свой второй параметр пару имя-значение из конфига. Как только пары кончатся, функция вернёт ноль. Функция str2num хоть и кажется, что переводит строку в число, но на деле она работает только с предопределёнными соответствиями, которые передаются в первом параметре. Например, bool_pair представлен примерно так { {"0", 0}, {"1", 1 } }.

Для того, чтобы пользователь смог задать настройки, мы должны показать диалог при обработке соответствующего события.
void lxpamonConfigure(Plugin * plugin, GtkWindow * parent)
{
PluginData * data = (PluginData *)plugin->priv;
GtkWidget * dlg = create_generic_config_dlg(_(plugin->class->name), GTK_WIDGET(parent),
(GSourceFunc)lxpamonApplyConfiguration, plugin,
_("Pulseaudio sink"), &data->sink, CONF_TYPE_INT,
_("Volume display format"), &data->format, CONF_TYPE_STR,
_("Use %i to print current volume. Use %% to print the % mark."), NULL, CONF_TYPE_TRIM,
_("Bold font"), &data->bold_font, CONF_TYPE_BOOL,
NULL);
gtk_window_present(GTK_WINDOW(dlg));
}
Здесь используется функция create_generic_config_dlg, показывающая стандартный диалог с настройками.

Вся проблема в том, что этой функции нет в файле lxpanel/plugin.h. Она объявлена в самой панели, но стандартные плагины используют именно её. Поэтому мы добавим себе её объявление.
extern GtkWidget * create_generic_config_dlg(const char * title, GtkWidget * parent,
GSourceFunc apply_func, Plugin * plugin, const char * name, ...);
Эта функция принимает родителя, заголовок и колбек, который будет вызван для применения настроек. В списке неопределённых параметров функция принимает тройки "имя параметра", "указатель на значение" и "тип значения". Завершается список параметров NULL`ом. Макрос _ объявлен в GLib и нужен для интернационализации. При принятии параметров мы устанавливаем новый sink для монитора и перерисовываем виджет
void lxpamonApplyConfiguration(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
pamonSetSinkId(plug_data->pamon, plug_data->sink);
lxpamonDrawVolume(plugin);
}

static void lxpamonDrawVolume(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
char * format = plug_data->format ? plug_data->format : "%%i";
char * buf = (char *)malloc(strlen(format) + 4);
if(INVALID_VOLUME == plug_data->current_volume)
strcpy(buf, "ERROR");
else
sprintf(buf, plug_data->format, plug_data->current_volume);
panel_draw_label_text(plugin->panel, plug_data->label, buf, plug_data->bold_font, TRUE);
free(buf);
}
При отрисовке мы снова используем функцию, которая не объявлена в lxpanel/plugin.h.
extern void panel_draw_label_text(Panel * p, GtkWidget * label, char * text,
gboolean bold, gboolean custom_color);
Функция panel_draw_label_text рисует текст на лейбле цветом, заданным в конфигурации панели. Структура Plugin содержит поле panel, у которого в свою очередь есть поля, хранящие цвета панели, в том числе цвет шрифта. Но есть и флаг usefontcolor, который установлен в 0, а при попытке использовать значение цвета получается что-то красное. Функция panel_draw_label_text также позволяет рисовать полужирным шрифтом, чем мы тоже пользуемся.
Значение INVALID_VOLUME устанавливается при обработке ошибки от pamon. В этом случае наш плагин напишет слово "ERROR".

Функция panel_draw_label_text не последняя из тех, которые забыли положить в файл lxpanel/plugin.h, ещё нам нужен стандартный обработчик клика мыши.
extern gboolean plugin_button_press_event(GtkWidget * widget, GdkEventButton * event, Plugin * plugin);
Эту функцию мы повесели на "button_press_event" в конструкторе. Эта функция добавляет в контекстное меню пункты для редактирования настроек и удаления виджета с панели.

После того, как настройки панели изменены нам нужно перерисовать свой виджет. Именно для этого мы и подписывались на это событие.
void lxpamonPanelConfigurationChanged(Plugin * plugin)
{
lxpamonDrawVolume(plugin);
}

Обработчики событий от pulseaudio тривиальны и не нуждаются в комментариях за одним исключением, ошибка может произойти до того, как поле plugin->priv будет присвоено, поэтому его нужно проверить.
void lxpamonPanelConfigurationChanged(Plugin * plugin)
{
lxpamonDrawVolume(plugin);
}

void pamonVolumeChanged(uint32_t current_volume, Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
plug_data->current_volume = current_volume;
lxpamonDrawVolume(plugin);
}

void pamonError(PamonError error, Plugin * plugin)
{
fprintf(stderr, "Pulseaudio monitor error: %s\n", pamonTranslateError(error));
PluginData * plug_data = (PluginData *)plugin->priv;
if(plug_data)
{
plug_data->current_volume = INVALID_VOLUME;
lxpamonDrawVolume(plugin);
}
}
Последнее, что осталось сделать -- освободить ресурсы при выгрузке плагина. Здесь это гораздо важнее, чем в консольном приложении, так как панель может продолжать работать ещё долгое время и утекшая память не вернётся системе.
void lxpamonDestructor(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
pamonClose(plug_data->pamon);
g_free(plug_data->format);
g_free(plug_data);
}
Поле plugin->pwid осовобождать не нужно, lxpanel это сделает за Вас.

P.S.

Исходники проекта лежат на github.
git clone https://github.com/brainstream/lxpamon.git
Возможно, проект я буду дорабатывать, если кому интересна изначальная версия, она лежит здесь.

Получившийся результат можно посмотреть на видео ниже

Virtual inline методы C++

C++ даёт возможность определять встраиваемые (inline) функции и методы. Реализация таких функций будет встраиваться в другие функции по месту вызова (за исключением некоторых случаев, когда это невозможно). Как правило, в конечном исполняемом файле или динамической библиотеке отдельной реализации таких функций не генерируется.
Другой замечательный механизм, предоставляемый языком C++ — это виртуальные методы. Эти методы участвуют в полиморфизме. Вы может вызывать такие функции из производного класс по указателю или ссылке на базовый. Достигается это путём помещения указателя на конкретную реализацию в таблицу виртуальных функций, которая помещается компилятором в известное ему место относительно полиморфного указателя или ссылке (конкретная реализация не стандартизирована и зависит от компилятора).

Стандарт C++ позволяет применять модификатор virtual к inline функциям. Тут мы наталкиваемся на неоднозначность. Как в таблицу виртуальных функций попадёт указатель, если тело функции не генерируется? Ответ на этот вопрос также не стандартизирован и зависит от компилятора. Давайте попробуем разобраться.
Итак, допустим у нас есть такой код
extern "C" void exFunction(int);

class Base
{
public:
virtual int doSomething(int x, int y)
{
return x + y;
}
};

class Child : public Base
{
public:
virtual int doSomething(int x, int y)
{
return x * y;
}
};

extern "C" void run()
{
Base base;
int result = base.doSomething(1, 2);
exFunction(result);
Child child;
result = child.doSomething(2, 2);
exFunction(result);
}
Функция exFunction нужна лишь для того, чтобы компилятор не соптимизировал наши вычисления.
Я буду использовать компилятор g++ 4.7.2 и операционную систему Debian GNU/Linux 7.0 (wheezy) 64 bit.
Сохраним исходный текст в файл test.cpp соберём динамическую библиотеку с помощью команды
g++ -g -O2 -shared -fPIC  test.cpp
Я собираю динамическую библиотеку, чтобы было меньше мусора в дампе. Кроме того, я включил оптимизацию и отладочные символы. Посмотрим результат компиляции с помощью программы objdump.
objdump -d a.out
00000000000006e0 <run>:
6e0: 48 83 ec 08 sub $0x8,%rsp
6e4: bf 03 00 00 00 mov $0x3,%edi
6e9: e8 e2 fe ff ff callq 5d0 <exFunction@plt>
6ee: bf 04 00 00 00 mov $0x4,%edi
6f3: 48 83 c4 08 add $0x8,%rsp
6f7: e9 d4 fe ff ff jmpq 5d0 <exFunction@plt>
Как видим, компилятор не обратил внимание на модификатор virtual и просто встроил inline методы в код, соптимизировав сложение и умножение.
Теперь добавим в код полиморфизма.
extern "C" void exFunction(int);

class Base
{
public:
virtual int doSomething(int x, int y)
{
return x + y;
}
};

class Child : public Base
{
public:
virtual int doSomething(int x, int y)
{
return x * y;
}
};

extern "C" void run()
{
Base base;
int result = base.doSomething(1, 2);
exFunction(result);
Child child;
result = child.doSomething(2, 2);
exFunction(result);
Base * polymorphic = &child;
result = polymorphic->doSomething(3, 3);
exFunction(result);
}
Скомпилируем и посмотрим на дизассемблированный код.
0000000000000a50 <run>:
a50: 48 83 ec 18 sub $0x18,%rsp
a54: bf 03 00 00 00 mov $0x3,%edi
a59: e8 e2 fe ff ff callq 940 <exFunction@plt>
a5e: 48 8b 05 e3 03 20 00 mov 0x2003e3(%rip),%rax # 200e48 <_DYNAMIC+0x228>
a65: bf 04 00 00 00 mov $0x4,%edi
a6a: 48 83 c0 10 add $0x10,%rax
a6e: 48 89 04 24 mov %rax,(%rsp)
a72: e8 c9 fe ff ff callq 940 <exFunction@plt>
a77: 48 8b 04 24 mov (%rsp),%rax
a7b: 48 89 e7 mov %rsp,%rdi
a7e: ba 03 00 00 00 mov $0x3,%edx
a83: be 03 00 00 00 mov $0x3,%esi
a88: ff 10 callq *(%rax)
a8a: 89 c7 mov %eax,%edi
a8c: e8 af fe ff ff callq 940 <exFunction@plt>
a91: 48 83 c4 18 add $0x18,%rsp
a95: c3 retq
a96: 90 nop
a97: 90 nop
a98: 90 nop
a99: 90 nop
a9a: 90 nop
a9b: 90 nop
a9c: 90 nop
a9d: 90 nop
a9e: 90 nop
a9f: 90 nop

0000000000000aa0 <_ZN4Base11doSomethingEii>:
aa0: 8d 04 16 lea (%rsi,%rdx,1),%eax
aa3: c3 retq
aa4: 90 nop
aa5: 90 nop
aa6: 90 nop
aa7: 90 nop
aa8: 90 nop
aa9: 90 nop
aaa: 90 nop
aab: 90 nop
aac: 90 nop
aad: 90 nop
aae: 90 nop
aaf: 90 nop

0000000000000ab0 <_ZN5Child11doSomethingEii>:
ab0: 89 f0 mov %esi,%eax
ab2: 0f af c2 imul %edx,%eax
ab5: c3 retq
ab6: 90 nop
ab7: 90 nop
Теперь мы видим, что компилятор сгенерировал реализации для виртуальных методов и использовал виртуальную таблицу для третьего вызова. При этом первые два вызова остались встроенными.

Вообще говоря, компилятор всегда будет генерировать тело inline функции, если где-либо в программе требуется её адрес (явно или неявно). При этом, Вы можете расчитывать на встраивание virtual inline функции, если не используете для её вызова указатель или ссылку.

Я провёл подобное исследование с Visual C++, и оказалось, что этот компилятор ведёт себя аналогичным образом.

Настройка графического окружения Linux на основе xmonad и xmobar

Так совпало, что меня заинтересовали две вещи: язык программирования Haskell и фреймовые (или тайловые) оконные менеджеры под Linux. И тут я обнаружил замечательный тайловый менеджер, написанный на Haskell. Я не смог пройти мимо и установил себе xmonad.
Эта статья будет не совсем обычная для этого блога. Здесь практически не будет программирования. Я просто покажу Вам как настроить окружение на основе оконного менеджера xmonad и панелей xmobar.

Приведу сразу же скриншоты своего окружения. На первом, моё рабочее пространство без открытых окон.
А на втором запущено несколько оконных приложений

Сразу после установки xmonad, Вы, скорее всего, не сможете даже запустить какое-либо приложение. Вам придётся сходить в терминал и прочесть man xmonad, где описаны все дефолтные сочетания клавиш. Дабы облегчить Вам задачу, сразу же приведу основные, без которых работать в среде невозможно:
  • Mod+p: открыть dmenu для запуска приложений;
  • Mod+Shift+Enter: открыть терминал;
  • Mod+Shift+c: закрыть текущее окно;
  • Mod+j: перевести фокус на следующее окно;
  • Mod+k: перевести фокус на предыдущее окно;
  • Mod+[1-9]: перейти на рабочее пространство [1-9];
  • Mod+Shift+[1-9]: перекинуть текущее окно на рабочее пространство [1-9];
  • Mod+q: перезагрузить xmonad;
  • Mod+Shift+q: выйти из xmonad.
Mod по умолчанию -- клавиша Alt.
Никаких баров с xmonad не поставляется, но этот оконный менеджер хорошо интегрируется с xmobar и dzen2.

Базовая настройка xmonad

Для начала настроим оконный менеджер, а затем перейдём к барам, трею и дополнительным скриптам.
Xmonad настраивается путём описания настроек на языке Haskell. Если Вы не знакомы с этим замечательным языком, я советую Вам исправить эту ситуацию как можно скорее. Без базового понимания Haskell, Вы сможете только скопировать готовый конфиг и кастомизировать его по аналогии с уже написанным кодом. В то время как зная Haskel, Вам откроются безграничные просторы для фантазии.
Настройки xmonad должны находиться в файле ~/.xmonad/xmonad.hs.
Минимальный файл конфигурации должен содержать функцию main, в которой происходит вызов функции xmonad с передачей структуры XConfig, содержащей конфигурацию.
import XMonad

main = do xmonad $ defaultConfig
XConfig проще всего получить из функции defaultConfig и на основе настроек по умолчанию создать свои собственные.
Первое, что мне захотелось сделать -- это изменить клавишу-модификатор  с Alt на Super, так как клавиша Alt используется многими другими приложениями. Сделать это очень легко, достаточно назначить полю modMask значение типа KeyMask, в нашем случае это mod4Mask.
import XMonad

main = do
xmonad $ defaultConfig {
modMask = mod4Mask }

Настройка xmobar и trayer

Теперь, когда мы знаем, как настраивать xmonad, было бы не плохо обзавестись тулбаром, который покажет время, загрузку CPU, а самое главное, информацию о том, где (в контексте xmonad) мы сейчас находимся. Кроме того, было бы здорово убрать почтовый клиент, jabber и прочие фоновые задачи в трей.
Xmobar -- это бар, который написан на haskell специально для xmonad, но может работать и без него. Настройки xmobar можно передавать через параметры напрямую или через конфигурационный файл. Я предпочитаю второй способ.
Для отображения трея будем использовать программу trayer.
Основная идея заключается в том, что я хочу, чтобы у меня справа отображались часы и раскладка клавиатуры, слева индикаторы загрузки процессора и памяти и состояние xmonad. Между правой и левой панелью я хочу поместить трей. Для реализации этой идеи мне понадобится запустить два экземпляра xmobar.
Мой монитор имеете ширину 1920 пикселов, поэтому я буду исходить из неё.
Конфигурационный файл xmobar имеет синтаксис конструктора с именованными полями языка haskell. Все его поля очень подробно описаны в man xmobar, поэтому я лишь кратко опишу свои файлы.
Итак, я создал каталог ~/local/conf и положил туда два файла xmobar_left и xmobar_right. Вот содержимое файла xmobar_right.
Config { font = "xft:DejaVu Sans Mono:size=10:bold:antialias=true",
bgColor = "#000000",
fgColor = "#BBBBBB",
position = Static { xpos = 1690 , ypos = 0, width = 230, height = 20 },
commands = [
Run Date "%a %b %_d %Y %H:%M:%S" "date" 10,
Run Kbd [("us", "US"), ("ru", "RU")]
],
template = "<fc=green>%kbd%</fc> %date%"
}
Назначение полей font, bgColor и fgColor не требует объяснений. Содержимое поля position было установлено опытным путём. Эксперименты показали, что для отображения даты и раскладки клавиатуры потребуется ровно 230 пикселов. Так как трей я хочу сделать шириной в 250 пикселов, то начальная позиция правой панели будет в точке 1690 по оси X. Высота бара будет состовлять 20 пикселов.
Самые важные поля в конфиге -- это commands и template. Поле commands представляет собой список команд, которые xmobar должен запустить. Список поддерживаемых команд можно найти в man xmobar. Команды имеют параметры. Команда Date принимает три аргумента: формат даты, переменная, которая будет использоваться в шаблоне для доступа к значению команды и частоту обновления в десятых долях секунды. Команда Kdb принемает список двуместных кортежей, описывающий доступные раскладки клавиатуры. Первый элемент кортежа -- это наименование раскладки, а второй -- отображаемое имя. В шаблоне к значению этой команды можно обратиться по предопределённому имени kdb.
В параметре template записывается шаблон выводимого текста. Переменные из параметра commands нужно использовать, обернув их в символы %. Кроме переменных можно использовать простой текст и разметку, указывающую цвет символов.
Далее приведу конфиг для левой панели.
Config { font = "xft:DejaVu Sans Mono:size=10:bold:antialias=true",
bgColor = "#000000",
fgColor = "#BBBBBB",
position = Static { xpos = 0 , ypos = 0, width = 1530, height = 20 },
commands = [ Run Cpu ["-S", "True", "-t", "CPU: <total>", "-L","5","-H","40","--normal","#FFFF00","--high","#FF0000"] 10,
Run Memory ["-S", "True", "-t","RAM: <usedratio> (<used>MiB of <total>MiB)"] 10,
Run XMonadLog
],
template = "%cpu% %memory% %XMonadLog%"
}
Здесь используются команды Cpu, Memory и XMonadLog. Команде Cpu передаётся два параметра. Первый параметр -- это набор опций для отображения. Шаблон передаётся через опцию -t (опция и её значение указываются в подряд следующих элементах списка). Доступны следующие переменные: total, bar, user, nice, system, idle, iowait, значение которых известно любому линуксойду. Опция -S в True просит xmobar показывать знак единицы измерения величины (в данном случае %). Опции -L и -H задают нижний и верхний предел для раскрашивания выводимого значения соответственно цветами в оциях --normal и --high. До значения -L цвет текста будет по умолчанию, между -L и -H цвет будет браться из опции --normal, для значений выше и равных -H будет использоваться цвет --high. Вторым параметром команде Cpu передаётся частота обновления в десятых долях секунды.
Команда Memory аналогична команде Cpu, только отображает загрузку памяти. Для этой команды доступны переменные total, free, buffer, cache, rest, used, usedratio, usedbar и freebar.
Самая интересная команда, которая тут используется -- это команда XMonadLog. Она не принимает никаких аргументов, а для её использования нам придётся кое-что написать в конфигурационном файле xmonad.
Запуск trayer`а достаточно тривиален.
trayer --edge top --align right --margin 230  --widthtype pixel --width 200 --heighttype pixel --height 20 --tint 0x0 --alpha 0 --transparent true
Думаю, что тут всё понятно и я не буду останавливаться на этой команде.
Теперь помещаем в файл ~/.xsession необходимые команды для запуска xmonad, xmobar`ов и trayer`а.
xmobar ~/local/conf/xmobar_left &
xmobar ~/local/conf/xmobar_right &
trayer --edge top --align right --margin 230 --widthtype pixel --width 200 --heighttype pixel --height 20 --tint 0x0 --alpha 0 --transparent true &
exec xmonad

Полная настройка xmonad

Если Вы перезапустите xmonad, то увидите, что окна перекрывают Ваш бар. Структура данных XConfig содержит несколько полей, которым можно передать функцию обработки некоторых событий. Для того, чтобы бары не перекрывались, следует определить функцию layoutHook и обернуть лейауты в конструктор AvoidStruts при помощи функции avoidStruts. Мы можем использовать лейауты по умолчанию, но лучше определим свой собственный список. Это позволит добавить собственные лейауты и настроить дефолтные.
import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
layoutHook = myLayouts }

Новый код выделен полужирным шрифтом. Итак, мы создаём 4 лейаута:
  1. Tall имеет 2 столбца. В левом отображаются основные окна, а в правом вспомогательные. Конструктор принимает три аргумента. Первый -- количество окон в основном (левом) столбце. Второй -- процент, на который изменяется размер столбцов при увеличении и уменьшении. Последний -- отношение ширины столбцов по умолчанию;
  2. StackTile. Все окна располагаются в вертикальном стеке. Параметры означают то же самое, что и в конструкторе Tall;
  3. simpleTabbed -- функция, которая создаёт лейаут, в котором все окна максимально развёрнуты, а сверху экрана появляются вкладки с их заголовками;
  4. Full. Все окна максимально развёрнуты.
Лейауты объединяются оператором |||. Объединённые лейауты передаём функции smartBorders, которая убирает рамку окна, если оно одно. Далее вызывается функция avoidStruts для предотвращения перекрытия баров. Полноэкранный лейаут должен перекрывать бары, поэтому он не должен попадать под действие avoidStruts. А так как окно там всегда одно, то вместо smartBorders, мы применяем к этому режиму функцию noBorders для предотвращения отрисовки рамки. Документацию и список доступных лейаутов можно найти здесь.

Ещё одна беда заключается в том, что панель trayer принимает фокус и присутствует лишь на одном workspace`е. Чтобы решить эту проблему, нужно добавить в обработчик manageHook функцию manageDocks.
import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
layoutHook = myLayouts,
manageHook = manageHook defaultConfig <+> manageDocks }
Хуки, как видно, объединяются оператором <+>. Кроме управления барами, в обработчике manageHook можно управлять поведением различных окон. Все доступные операции приведены в документации здесь. Я покажу, на примере своего конфига, как делать определённые окна плавающими.
myManageHook = composeAll [
className =? "Gtk-recordmydesktop" --> doFloat,
className =? "Xmessage" --> doFloat,
className =? "Gxmessage" --> doFloat,
className =? "Galculator" --> doFloat,
className =? "Gksu" --> doFloat ]
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook
Здесь я получаю класс окна при помощи функции className и сравниваю его с образцом оператором =?. Если образец и класс совпадают, то окно передаётся функции doFload оператором -->, которая делает окно плавающим. Класс окна можно получить командой
$ xprop | grep CLASS
Кроме класса окна можно использовать имя приложения или заголовок окна. Все доступные операции описаны в документации.

В обработчике manageHook можно ловить окна и перенаправлять их на другой workspace по его ID функцией doShift. Идентификаторы (они же имена) передаются в XConfig в параметре workspaces, который является списком строк.
myWorkspaces = [ "1", "2", "3", "4", "5", "6", "7", "8", "9" ]
main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
workspaces = myWorkspaces,
layoutHook = myLayouts,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook }
Конечно же Вы можете назвать ваши рабочие пространства как хотите. Эти имена будут отображаться в нашем xmobar.

Вот и пришло время настроить тот самый лог xmonad, который мы использовали в левом xmobar. Как сказано в man xmobar, мы должны указать в параметре logHook такое выражение
logHook = dynamicLogString defaultPP >>= xmonadPropLog
И это будет работать. Но вывод, предоставленный функцией defaultPP отображается одним цветом и немного неудобно (для меня) отформатирован. Функция dynamicLogString принимает экземпляр типа PP (pretty-printing) и отдаёт строку, отформатированную согласно этому параметру. Создать экземпляр PP лучше всего функцией xmobarPP.
myLog = dynamicLogString xmobarPP {
ppCurrent = xmobarColor "green" "" . wrap "[" "]",
ppTitle = xmobarColor "lightblue" "" . wrap "[" "]",
ppHidden = xmobarColor "yellow" "",
ppHiddenNoWindows = xmobarColor "darkgray" "",
ppLayout = xmobarColor "orange" "" }
logHook = myLog >>= xmonadPropLog,
Для работы этого кода необходимо подключить модуль XMonad.Hooks.DynamicLog.
Итак, лог выглядит следующим образом:
<workspace`ы> : <имя лейаута> : <заголовок текущего окна>
Разделителем по умолчанию между воркспейсами служит пробел, а между воркспейсами, именем лейаута и именем окна -- двоеточие, окружённое пробелами. Разделители меня устраивают. В структуре PP мы меняем следующие параметры:
  • ppCurrent: имя текущего workspace`а. Его я отображаю зелёным цветом при помощи функции xmobarColor и оборачиваю в символы [ и ] стандартной функцией haskell wrap;
  • ppTile: заголовок текущего окна;
  • ppHidden: имена воркспейсов, на которых есть окна, но которые не являются текущими;
  • ppHiddenNoWindows: не текущие воркспейсы, на которых нет окон (по умолчанию не отображаются вообще);
  • ppLayout: имя лейаута.

Раз уж мы начали заниматься красивосятми, то, мне кажется, нужно поменять и рамку окон xmonad. Сделать это можно передачей параметров borderWidth, normalBorderColor и focusedBorderColor в XConfig.
main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",

workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook }

Я использую клавиатуру с кучей мультимедийных клавиш и хотел бы использовать некоторые из них в повседневной работе. Xmonad позволяет очень легко привязать обработчики к любым клавишам. Привязки клавиш  передаются в XConfig параметром keys.
myKeys x = M.union (keys defaultConfig x) (keysToAdd x) 
where
keysToAdd = c -> mkKeymap c $ [
("<XF86HomePage>", spawn "x-www-browser"),
("<XF86AudioRaiseVolume>", spawn "~/local/bin/pactrl.sh inc"),
("<XF86AudioLowerVolume>", spawn "~/local/bin/pactrl.sh dec"),
("<XF86AudioMute>", spawn "~/local/bin/pactrl.sh mute"),
("<XF86AudioPlay>", spawn "xterm -e cmus"),
("<XF86Mail>", spawn "icedove"),
("<XF86Search>", spawn "pcmanfm"),
("<XF86Calculator>", spawn "galculator"),
("<XF86Launch5>", spawn "emacs"),
("<XF86Launch6>", spawn "gthumb"),
("<XF86Favorites>", spawn "gksu halt"),
("M-<XF86Favorites>", spawn "gksu reboot"),
("<Print>", spawn "~/local/bin/printscreen.sh"),
("M1-<Tab>", windows W.focusDown),
("M1-<F4>", kill) ]
main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",
workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook,
keys = myKeys }
Для работы кода требуется импортировать следующие модули:
import XMonad.Util.EZConfig
import XMonad.Operations
import qualified XMonad.StackSet as W
import qualified Data.Map as M
Привязки клавиш представляют собой карту кортежей из модификатора, клавиши и обработчика. Существует несколько способов создать такую карту, но я предпочитаю использовать функцию mkKeymap. Функция mkKeymap принимает список кортежей состоящий из строкового обозначения клавиатурного сочетания в стиле emacs и обработчика. Получить имена клавиш можно с помощью программы xev или подглядеть их в минибуфере emacs.
Функция spawn запускает внешнее приложение.
Для управления окнами используются два модуля: XMonad.Operations и XMonad.StackSet. Первый предоставляет API высокого уровня для работы с окнами, а второй позволяет работать непосредственно со стеком окон и экранами.
Функция focusDown принимает стек окон, перемещает фокус на следующее окно и возвращает новый стек. Но обработчики клавиатурных сочетаний должны возвращать тип X (). Здесь нам поможет функция windows, которая принимает функцию, принимающую и возвращающую стек окон, и возвращает нужный нам X ().
Чтобы закрыть текущее окно, достаточно применить функцию kill. Она сразу же возвращает необходимое значение, поэтому оборачивать её ни во что не нужно.
Получив карту клавиатурных сочетаний, её нужно объединить с картой по умолчанию. Для этого используется функция union из модуля Data.Map.

Xmobar, по умолчанию, предоставляет одно сочетание клавиш для вызова терминала -- Mod+Shift+Enter. Изменить терминал можно через параметр terminal.

Полный конфигурационный файл ~/.xmonad/xmonad.hs

import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile
import XMonad.Hooks.DynamicLog
import XMonad.Util.EZConfig
import XMonad.Operations
import qualified XMonad.StackSet as W
import qualified Data.Map as M

myWorkspaces = [ "1", "2", "3", "4", "5", "6", "7", "8", "9" ]

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

myManageHook = composeAll [
className =? "Gtk-recordmydesktop" --> doFloat,
className =? "Xmessage" --> doFloat,
className =? "Gxmessage" --> doFloat,
className =? "Galculator" --> doFloat,
className =? "Gksu" --> doFloat ]

myLog = dynamicLogString xmobarPP {
ppCurrent = xmobarColor "green" "" . wrap "[" "]",
ppTitle = xmobarColor "lightblue" "" . wrap "[" "]",
ppHidden = xmobarColor "yellow" "",
ppHiddenNoWindows = xmobarColor "darkgray" "",
ppLayout = xmobarColor "orange" "" }

myKeys x = M.union (keys defaultConfig x) (keysToAdd x)
where
keysToAdd = c -> mkKeymap c $ [
("<XF86HomePage>", spawn "x-www-browser"),
("<XF86AudioRaiseVolume>", spawn "~/local/bin/pactrl.sh inc"),
("<XF86AudioLowerVolume>", spawn "~/local/bin/pactrl.sh dec"),
("<XF86AudioMute>", spawn "~/local/bin/pactrl.sh mute"),
("<XF86AudioPlay>", spawn "xterm -e cmus"),
("<XF86Mail>", spawn "icedove"),
("<XF86Search>", spawn "pcmanfm"),
("<XF86Calculator>", spawn "galculator"),
("<XF86Launch5>", spawn "emacs"),
("<XF86Launch6>", spawn "gthumb"),
("<XF86Favorites>", spawn "gksu halt"),
("M-<XF86Favorites>", spawn "gksu reboot"),
("<Print>", spawn "~/local/bin/printscreen.sh"),
("M1-<Tab>", windows W.focusDown),
("M1-<F4>", kill) ]

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
terminal = "xterm",
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",
workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook,
keys = myKeys }

Немного скриптов

Как Вы могли заметить, я привязал к клавишам два скрипта: один регулирует громкость PulseAudio, другой снимает скриншоты. Приведу их тут с кратким описанием для полноты картины.
#!/bin/bash

VOLUME_FILE=/tmp/pa_volume
MUTE_FILE=/tmp/pa_mute

function doMute() {
if [ -f $MUTE_FILE ];
then
pactl set-sink-mute 0 0
rm -rf $MUTE_FILE
else
pactl set-sink-mute 0 1
touch $MUTE_FILE
fi
}

function doChangeVolume() {
if [ ! -f $VOLUME_FILE ];
then
echo 100 > $VOLUME_FILE
fi
VOLUME=`cat $VOLUME_FILE`
NEW_VOLUME=$((VOLUME+$1))
if [ $NEW_VOLUME -lt 0 ];
then
exit 0
fi
pactl set-sink-volume 0 $NEW_VOLUME%
echo $NEW_VOLUME > $VOLUME_FILE
}

case $1 in
inc)
doChangeVolume 2
;;
dec)
doChangeVolume -2
;;
mute)
doMute
;;
esac
exit 0
Скрипт использует утилиту pactl для изменения громкости. Можно было бы использовать её напрямую в xmonad.hs, но существуют три проблемы:
  1. Из-за ошибки в программе, невозможно передать ей отрицательное значение для изменения громкости; она воспринимает его как неизвестный параметр;
  2. После того, как громкость достигла нуля, pactl продолжает её уменьшать, после чего придётся увеличивать её несколько раз, чтобы достичь нуля;
  3. Программа не предоставляет возможности узнать, в каком состоянии находится mute.
Примечание: по прошествии времени после публикации статьи, все проблемы с pactl решены.
  • Первая решается отменой восприятия "-2%" как параметра с помощью команды pactl set-sink-volume 0 -- -2%;
  • Вторая проблема решена в последней версии pulseaudio;
  • Последняя проблема не актуальна, так как появилась возможность попросить переключение состояния mute командой pactl set-sink-mute 0 toggle.
Скрипт записывает текущую громкость (в процентах) в файл и при поступлении команды либо увеличивает её, либо уменьшает. Для индикации состояния mute используется создание и удаление файла с предопределённым именем.

Для снятия скриншота используется конвеер
xwd -root | convert - <filename>
Скрипт нужен лишь для того, чтобы определить имя файла и показать его имя в диалоге с предложением открыть в просмоторщике.
#!/bin/bash

FILEPATH=~/
FILENAME=snapshot
FILEEXT=.png
FULLFILENAME=$FILEPATH$FILENAME$FILEEXT

if [ -f /usr/bin/gxmessage ];
then
XMESSAGE=gxmessage
else
XMESSAGE=xmessage
fi

for((I=1; ;I++)); do
if [ ! -f $FULLFILENAME ]; then
break
fi
FULLFILENAME=$FILEPATH$FILENAME$I$FILEEXT
done

xwd -root | convert - $FULLFILENAME
$XMESSAGE "Snapshot has been saved to $FULLFILENAME" -center -default Ok -buttons Ok:0,Open:1

if [ $? -eq 1 ];
then
gthumb $FULLFILENAME
fi

На скриншоте Вы могли заметить, что у меня на фоне корневого окна установлены обои. Делается это при помощи утилиты feh в два этапа. Первый -- это непосредственно установка изображения
$ feh --bg-scale <имя файла>
А второй -- восстановление изображения после перезапуска X`ов. Для этого нужно поместить такую команду в файл ~/.xsession
eval $(cat ~/.fehbg)

Почему C# хуже C++?

С момента моего ознакомительного исследования C# прошло полтора года. Поработав с языком, я убедился в превосходстве C++ над ним. Когда я говорю об этом, мои оппоненты часто начинают приводить доводы, касательно того, как хорош .NET или, что архитектура CLR накладывает ограничения на язык. Ни тот, ни другой аргументы не могут оправдать C#, так как язык -- это то, что программисты видят каждый день, а CLR скрыт от глаз и не должен оказывать влияния на инструмент разработки, как архитектура процессора не оказывает влияния на C++. В этой статье я не буду обсуждать стандартную библиотеку и рантайм, речь пойдёт только о языке. Не затягивая со вступлением, начну с самых основ.
Якщо вам потрібен якісний сайт або портал, то рекомендуємо звернутися у компанію  web-ukraine, вони допоможуть усе зробити по вашим бажанням, створення сайтів, підтримка сайтів, реклама сайтів все це входить у вартість послуг.

ООП ради ООП

Программируя долгое время на C++, я не задумывался о минусах ОО подхода. Один мой знакомый постоянно спорил со мной, утверждая, что ООП -- это зло. Тогда я не понимал истинных причин его выводов. Лишь начав программировать на C#, я осознал всю несостоятельность объектно-ориентированной парадигмы. Дело в том, что пока я писал на C++, я активно пользовался процедурной парадигмой и ООП лишь помогало там, где оно имело место. В C# (как впрочем и в Java) объектно-ориентированная парадигма вытеснила все другие. В итоге в C# мы имеем множество статических классов-Helper`ов и Utility. Гипертрофия ООП в C# приводит к тому, что классы содержат невероятно избыточное количество данных для настройки своего поведения и поведения своих потомков. Подробнее о недостатках ООП подхода можно почитать здесь. Приведу одну цитату:

Единственный способ сделать повторно используемые бизнес компоненты на этом уровне — сделать их сверх-настраиваемыми путём добавления таких штук как движки правил и встраиваемые языки. Такую модель я бы не стал называть моделью компонента. Скорее моделью очередного экземпляра bloatware. Обещание о повторном использовании ушло, люди либо покупали огромные bloatware системы(неудачники), или разрабатывали свои специальные бизнес объекты в своих собственных проектах.

Кроме того, Вы можете ознакомиться со знаменитой статьёй Ричарда Гэбриэла "Объектная парадигма провалилась" и её триумфальное возрождением "Почему объектно-ориентированное программирование провалилось?". Советую обратить внимание ещё и на эту статью. Подводя итог вышеперечисленным статьям, можно сказать, что концепция ООП не справилась с возложенными на неё ожиданиями, вместо этого она усложнила разработку и архитектуру приложений. C++ же не настаивает на использовании ООП и позволяет получить от него максимум выгоды, используя там, где это оправдано. Доклад Джека Дидриха довольно точно описывает моё отношение к ООП: не нужно писать класс, если можно написать функцию. Исходя из того, что C# ничего, кроме ООП не предлагает, в дальнейшем я буду рассматривать C# и C++ с точки зрения объектно-ориентированной парадигмы.

Наследование

Наследование в C++ -- это одна из самых широких возможностей. C++ позволяет использовать один из нижеприведённых вариантов или любую их комбинацию:

  • Наследование интерфейса;
  • Наследование реализации;
  • Множественное наследование;
  • Виртуальное наследование.

C# изо всего этого поддерживает лишь наследование интерфейса, наследование интерфейса вместе с реализацией и множественное наследование интерфейсов без реализации. Вообще говоря, множественное наследование интерфейсов с реализациями используется довольно редко, а виртуальное ещё реже. Но при возникновении такой необходимости в C# приходится применять сложные обходные пути. К сожалению, реальные примеры слишком сложны, а книжные мало убедительны, тем не менее, я постараюсь показать полезность множественного наследования на небольшом примере. Предположим, что в программе имеется класс Clock (часы) и класс Phone (телефон).

class Clock
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone
{
public:
    PhoneStream * call(const std::string & number);
};

Естественно, у методов должна быть реализация и методов может быть гораздо больше. Теперь Вам необходимо написать класс CellPhone (мобильный телефон). В C++ Вы могли бы использовать множественное наследование для получения функционала часов и телефона в одном устройстве.

class CellPhone : public Clock, public Phone { };

Вместо одной строки кода, на C# придётся написать

class CellPhone {
    Phone phone = new Phone();
    Clock clock = new Clock();

    public Phone Phone {
        get { return phone; }
    }

    public Clock Clock {
        get { return clock; }
    }
}

При такой реализации клиенты этого класса будут вынуждены углубить связность с его внутренней реализацией, что противоречит закону Деметры. Чтобы избежать связности можно реализовать все методы классов Clock и Phone внутри класса CellPhone, но это имеет смысл только при условии, что указанные классы имеют интерфейсы и используются только через них. Теперь усложним задачу. Каждый класс, Clock и Phone, будут унаследованы от класса Device (устройство).

class Device
{
public:
    void powerOn();
    void powerOff();
};

class Clock : public Device
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone: public Device
{
public:
    PhoneStream * call(const std::string & number);
};

class CellPhone : public Clock, public Phone { };

В этом коде есть одна, не слишком очевидная, проблема. Объекты класса CellPhone будут иметь два экземпляра класса Device: один от Clock, другой от Phone. Для таких случаев (которые получили название ромбовидное наследование) в C++ введён специальный тип наследования -- виртуальное наследование.

class Device
{
public:
    void powerOn();
    void powerOff();
};

class Clock : public virtual Device
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone: public virtual Device
{
public:
    PhoneStream * call(const std::string & number);
};

class CellPhone : public Clock, public Phone { };

Теперь объекты CellPhone будут иметь один экземпляр класса Device. C# не предоставляет подобной возможности. В агрегированных объектах у Вас всегда будут храниться два разных объекта класса Device, бездарно расходуя память. Подробно о пользе и вреде множественного наследования  Вы можете прочитать в книге Скотта Майерса "Эффективное использование C++. 50 рекомендаций по улучшению Ваших программ и проектов" в правиле 42: "Продумывайте подход к использованию множественного наследования". Более реальный пример множественного наследования  -- реализация паттерна проектирования Observer (наблюдатель).

#include <iostream>
#include <set>

template<typename T>
class Observable
{
protected:
    Observable() { }
    virtual ~Observable() { }

public:
    bool addObserver(T * observer)
    {
        return m_observers.insert(observer).second;
    }

    bool removeObserver(T * observer)
    {
        return m_observers.erase(observer);
    }

protected:
    std::set<T *> m_observers;
};

class Worker
{
public:
    virtual ~Worker() { }
    virtual void start() = 0;
    virtual void stop() = 0;
};

class WorkerObserver
{
public:
    virtual ~WorkerObserver() { }
    virtual void onStart() = 0;
    virtual void onStop() = 0;
};

class ObservableWorker : public Observable<WorkerObserver>, public Worker
{
public:
    virtual void start()
    {
        for(std::set<WorkerObserver *>::iterator it = m_observers.begin();
            m_observers.end() != it; ++it)
        {
            (*it)->onStart();
        }
    }

    virtual void stop()
    {
        for(std::set<WorkerObserver *>::iterator it = m_observers.begin();
            m_observers.end() != it; ++it)
        {
            (*it)->onStop();
        }
    }
};

class WorkerLogger : public WorkerObserver
{
public:
    virtual void onStart()
    {
        std::cout << "Started" << std::endl;
    }

    virtual void onStop()
    {
        std::cout << "Stoped" << std::endl;
    }
};


int main()
{
    ObservableWorker worker;
    WorkerLogger logger;
    worker.addObserver(&logger);
    worker.start();
    worker.stop();
    std::cin.get();
    return 0;
}

В данном подходе класс ObservableWorker является одновременно и Worker`ом и Observable`ом. Без множественного наследования пришлось бы выбирать между родителями, и выбор был бы в пользу Worker`а, а методы addObserver и removeObserver пришлось бы реализовавать в каждом классе. Кроме упомянутых методов, класс Observable мог бы содержать дополнительные методы работы с коллекцией наблюдателей, но для краткости я не стал раздувать код примера.

Ещё один вид наследования, который оказался за бортом C# -- наследование реализации без интерфейса или, проще говоря, приватное наследование. Этот вид наследования не делает унаследованный класс потомком родителя и не может участвовать в его полиморфных операциях. По большому счёту, закрытое наследование мало отличается от реализации класса посредством агрегации объекта некоторого другого класса (того, от которого производится наследование). Но в закрытом наследовании есть один большой плюс: оно предоставляет доступ ко всем защищённым членам базового класса. Один из вариантов использования закрытого наследования приведён в книге Скотта Майерса "Эффективное использование C++. 50 рекомендаций по улучшению Ваших программ и проектов" в правиле 42: "Продумывайте подход к использованию закрытого наследования". Этот пример довольно интересен, но является специфичным для C++, поэтому тут я его приводить не буду.

Инкапсуляция

Один из самых любимых мною, да и всеми C++ программистами, паттерн Private Implementation (например, библиотека Qt очень активно использует этот паттерн) не применим в C# вообще. Причиной этому является открытость всех модулей. В C++, желая скрыть детали имплементации класса, мы можем предварительно объявить в заголовочном файле приватный внутренний класс и указатель на него, а сам интерфейс и реализацию перенести в *.cpp модуль. Тем самым мы скрываем все переменные и дополнительные, вспомогательные методы, которые мы используем, от посторонних глаз. Интерфейс остаётся чистым и понятным при реализации любой сложности. В описанной стратегии очень сложно достучаться до деталей приватной имплементации. В C# при помощи reflection можно вызвать любой приватный метод и изменить приватное состояние объекта всего в одну-две строки кода. Но этот соблазн ещё выше с учётом того, что все внутренности класса находятся в одном модуле.

Объявление синонимов типа

Создатели C# посчитали, что оператор typedef им не нужен. В C++ оператор typedef служит для объявления синонимов типов. Для чего это нужно? К примеру, Вы используете в качестве параметра метода такой тип

Tuple<Dictionary<string, List<MyObject>>, bool>

Вместо того, чтобы всякий раз писать эту длинную последовательность, оператор typedef позволяет сократить запись. В C# это могло бы выглядеть так

typedef Tuple<Dictionary<string, List<MyObject>>, bool> MyType;

И далее, везде, где использовался изначальный длинный тип, можно было бы писать просто MyType. Кроме удобочитаемого имени, оператор typedef используется в C++ для получения типов из других классов или шаблонов. Приведу простой пример использования.

#include <iostream>
#include <vector>

struct Letter
{
    typedef char ValueType;
    char value;
};

struct Number
{
    typedef int ValueType;
    int value;
};

template<typename Element>
class Container
{
public:
    typedef typename Element::ValueType  ElementValueType;

    void add(ElementValueType value)
    {
        Element element;
        element.value = value;
        m_elements.push_back(element);
    }

    void print()
    {
        for(typename std::vector<Element>::const_iterator it = m_elements.begin();
            m_elements.end() != it; ++it)
        {
            std::cout << it->value << " ";
        }
    }
    
private:
    std::vector<Element> m_elements;
};


void testLetter()
{
    Container<Letter> container;
    container.add('a');
    container.add('b');
    container.add('c');
    std::cout << "Letters: ";
    container.print();
    std::cout << std::endl;
}

void testNumber()
{
    Container<Number> container;
    container.add(1);
    container.add(2);
    container.add(3);
    std::cout << "Numbers: ";
    container.print();
    std::cout << std::endl;
}

int main()
{
    testLetter();
    testNumber();
    std::cin.get();
}
Letters: a b c
Numbers: 1 2 3

Класс Container получает из типа шаблона тип значения, которое хранится в некотором другом классе и которое доступно из переменной value. Далее класс Container может оперировать этим типом, в том числе и в параметрах методов. Typedef`ы типов могут быть вложены и тогда классы Number и Letter могут стать одним шаблоном. Работают такие конструкции во время компиляции, что обеспечивает почти полную защиту от ошибок. Более сложные примеры с большой вложенностью Вы сможете найти в заголовочных файлах контейнеров (std::vector, std::map итд) стандартной библиотеки шаблонов (STL) С++.

Обобщения

Одним из самых мощных инструментов в C++ являются шаблоны. Сразу приведу пример, с которым я недавно столкнулся. Представим, что у нас есть абстрактный класс Connection

class Connection
{
public:
    virtual ~Connection() { }
    virtual bool connect(const std::string & address, short port) = 0;
    virtual void disconnect() = 0;
    virtual bool isConnected() const = 0;
    virtual void send(char * buffer, size_t size) = 0;
    virtual size_t recv(char * buffer, size_t size) = 0;
};

И от него унаследованы два (или более) конкретных класса: TcpConnection и SshConnection

class TcpConnection : public Connection
{
public:
    inline TcpConnection();
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
    virtual bool isConnected() const;
    virtual void send(char * buffer, size_t size);
    virtual size_t recv(char * buffer, size_t size);

private:
    bool m_is_connected;
};

TcpConnection::TcpConnection() :
    m_is_connected(false)
{
}

bool TcpConnection::connect(const std::string & address, short port)
{
    std::cout << "TcpConnection::connect" << std::endl;
    m_is_connected = ::rand() % 2;
    return m_is_connected;
}

void TcpConnection::disconnect()
{
    std::cout << "TcpConnection::disconnect" << std::endl;
    m_is_connected = false;
}

bool TcpConnection::isConnected() const
{
    std::cout << "TcpConnection::isConnected" << std::endl;
    return m_is_connected;
}

void TcpConnection::send(char * buffer, size_t size)
{
    std::cout << "TcpConnection::send" << std::endl;
}

size_t TcpConnection::recv(char * buffer, size_t size)
{
    std::cout << "TcpConnection::recv" << std::endl;
    return size;
}
class SshConnection : public Connection
{
public:
    inline SshConnection();
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
    virtual bool isConnected() const;
    virtual void send(char * buffer, size_t size);
    virtual size_t recv(char * buffer, size_t size);

private:
    bool m_is_connected;
};

SshConnection::SshConnection() :
    m_is_connected(false)
{
}

bool SshConnection::connect(const std::string & address, short port)
{
    std::cout << "SshConnection::connect" << std::endl;
    m_is_connected = ::rand() % 2;
    return m_is_connected;
}

void SshConnection::disconnect()
{
    std::cout << "SshConnection::disconnect" << std::endl;
    m_is_connected = false;
}

bool SshConnection::isConnected() const
{
    std::cout << "SshConnection::isConnected" << std::endl;
    return m_is_connected;
}

void SshConnection::send(char * buffer, size_t size)
{
    std::cout << "SshConnection::send" << std::endl;
}

size_t SshConnection::recv(char * buffer, size_t size)
{
    std::cout << "SshConnection::recv" << std::endl;
    return size;
}

Для примера, я не стал загромождать реализацию. Теперь стоит задача написать адаптер для каждого из конкретных классов, перекрыв методы connect и diconnect. В C++ это очень просто, нужно лишь написать шаблонный класс и отнаследовать его от параметра этого шаблона.

template<typename T>
class ConnectionAdapter : public T
{
public:
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
};

template<typename T>
bool ConnectionAdapter<T>::connect(const std::string & address, short port)
{
    bool connected = T::connect(address, port);
    std::cout << "Log: " << (connected ? "Connected" : "Connection failed") << std::endl;
    return connected;
}

template<typename T>
void ConnectionAdapter<T>::disconnect()
{
    bool connected = T::isConnected();
    T::disconnect();
    std::cout << "Log: " << (connected ? "Disconnected" :
        "Doesn't disconnected because connection hasn't been established") << std::endl;
}

Теперь объекты класса ConnectionAdapter можно использовать точно так же, как и объекты типа его параметра.

void tcpFunction(TcpConnection & connection)
{
    connection.connect("example.com", 80);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}

void sshFunction(SshConnection & connection)
{
    connection.connect("example.com", 22);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}

void commonFunction(Connection & connection)
{
    connection.connect("example.com", 123);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}


int main()
{
    ::srand(::time(NULL));

    ConnectionAdapter<TcpConnection> tcp_connection;
    ConnectionAdapter<SshConnection> ssh_connection;
    tcpFunction(tcp_connection);
    std::cout << std::endl;
    sshFunction(ssh_connection);
    std::cout << std::endl;
    commonFunction(tcp_connection);
    std::cout << std::endl;
    commonFunction(ssh_connection);
    std::cin.get();
    return 0;
}

Вы не сможете сделать нечто подобное на C#. C# не позволяет наследоваться от параметра обобщения. Таким образом, единственное, что Вам оставляет этот язык -- унаследовать базовый класс, Connection, реализовать все его абстрактные методы при помощи агрегированного объекта конкретного класса, и дописать в двух нужных методах по паре строк кода. И даже сделав это, Вы не сможете использовать объекты данного класса как объекты конкретного класса. Кроме приведённого примера, есть ещё одна очень удобная возможность использовать подобный механизм в купе с приватным наследованием. Передавая в шаблон некоторый тип Вы изменяете реализацию для шаблонного класса, хотя, как уже было сказано, можно заменить это простой агрегацией. Другим серьёзным недостатком обобщений C# является невозможность специализации. Если Вы точно знаете, что для определённого типа данных Ваш шаблон будет работать крайне не оптимально, то Вы можете создать для этого типа отдельную реализацию. C++ позволяет специализировать как все, так и любое количество аргументов шаблона.

#include <iostream>
#include <string>

template<typename T1, typename T2>
class Tuple
{
public:
    Tuple(const T1 & p1, const T2 & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    T1 m_p1;
    T2 m_p2;
};

template<typename T>
class Tuple<T, int>
{
public:
    Tuple(const T & p1, const int & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << "T, int specialization: " << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    T m_p1;
    int m_p2;
};

template<>
class Tuple<std::string, int>
{
public:
    Tuple(const std::string & p1, const int & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << "std::string, int specialization: " << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    std::string m_p1;
    int m_p2;
};


int main()
{
    Tuple<std::string, std::string>("Hello", "World").print();
    Tuple<int, int>(100, 42).print();
    Tuple<std::string, int>("Hello", 42).print();
    std::cin.get();
    return 0;
}

Этот код даст на выходе такой результат

Hello, World
T, int specialization: 100, 42
std::string, int specialization: Hello, 42

Как видно, компилятор выбирает наиболее подходящую реализацию исходя из параметров шаблона. К сожалению, для специализации шаблонов необходимо полностью повторить интерфейс и реализацию шаблона. Но, имея такой мощный инструмент, как препроцессор, в C++ это не является серьёзной проблемой. Справедливости ради, стоит отметить, что обобщения C# строго типизированы и поддерживают ковариантность (out) и контравариантность (in) аргументов, что обеспечивает поддержку наследования. Для того, чтобы C++ поддерживал эти механизмы необходимо вручную писать операторы преобразования типов.

Препроцессор

Препроцессор в C и C++ является одним из важнейших механизмов генерации кода. Роль препроцессора в C# ограничена лишь условной компиляцией и выдачей предупреждений и ошибок во время компиляции. C и C++ предлагают механизмы генерации кода с помощью препроцессора. Скажем, приложение использует пользовательский тип исключений

class Exception
{
public:
    Exception() :
        m_id(0)
    {
    }
    
    explicit Exception(int id) :
        m_id(id)
    {
    }

    Exception(int id, const std::string & message) :
        m_id(id),
        m_message(message)
    {
    }

private:
    int m_id;
    std::string m_message;
};

И вместо того, чтобы каждый раз, когда необходимо создать наследника этого класса, писать реализацию всех конструкторов, просто вызывающих базовый конструктор (думаю, что все сталкивались с этой задачей в C#), С++ позволяет написать один макрос.

#define DECLARE_EXCEPTION(name)                     \
    class name : public Exception                   \
    {                                               \
    public:                                         \
        name() { }                                  \
        name(int id) :                              \
            Exception(id)                           \
        {                                           \
        }                                           \
        name(int id, const std::string & message) : \
            Exception(id, message)                  \
        {                                           \
        }                                           \
    };

Всё, что нужно написать для определения двух классов -- две строки.

DECLARE_EXCEPTION(NotFoundException)
DECLARE_EXCEPTION(InvalidArgumentException)

Современные IDE, включая Visual Studio, способны выдавать подсказки по сгенерированному таким образом коду.

Неизменяемость

C# ограничивает понятие константности только лишь статическими ссылками на примитивные типы (POD),  дополнительно вводит ключевое слово readonly для того, чтобы ссылке нельзя было переприсвоить объект. Этих механизмов не достаточно, чтобы обеспечить безопасную манипуляцию объектами и избавиться от side-эффектов. C++ предлагает механизм неизменяемых объектов (равно как и переменных POD типов). Если Вы объявляете переменную или параметр функции с модификатором const, то в дальнейшем Вы не можете ни переприсвоить этой переменной значение, ни вызвать из такого объекта метод, который может изменить этот объект. Кроме того, модификатор const может быть применён к типу возвращаемого значения функции. Таким образом, можно реализовать безопасный возврат ссылки или указателя с ограничением только на чтение. Методы, которые не меняют состояние объекта помечаются модификатором const в конце сигнатуры. Внутри константных методов все члены-данные класса неявным образом становятся константами, за исключением тех, что помечены специальным модификатором mutable. Модификатор mutable говорит, что данная переменная не оказывает влияние на состояние объекта и может быть изменена в константных методах. Примером такой переменной может быть счётчик обращений к какому-либо методу. Следующий код вполне валиден  для C#.

class Person {
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override string ToString() {
        return String.Format("{0}, {1}", LastName, FirstName);
    }
}

class Account {
    readonly Person person;

    public Account(Person person) {
        this.person = person;
    }

    public Person Person {
        get { return person; }
    }
}

class Program {
    static void Main(string[] args) {
        Account account = new Account(new Person {
            FirstName = "John",
            LastName = "Connor"
        });
        account.Person.FirstName = "Isaac";
        account.Person.LastName = "Asimov";
        Console.Out.WriteLine(account.Person);
        Console.ReadKey();
    }
}

Этот пример демонстрирует изменение данных, которые, очевидно, не должны меняться. Следующий пример демонстрирует, как C++ легко обходит указанную проблему.

#include <string>

class Person
{
public:
    Person(const std::string & first_name, const std::string & last_name) :
        m_first_name(first_name),
        m_last_name(last_name)
    {
    }

    void setFirstName(const std::string & first_name)
    {
        m_first_name = first_name;
    }

    const std::string & firstName() const
    {
        return m_first_name;
    }

    void setLastName(const std::string & last_name)
    {
        m_last_name = last_name;
    }

    const std::string & lastName() const
    {
        return m_last_name;
    }

private:
    std::string m_first_name;
    std::string m_last_name;
};

class Account
{
public:
    Account(const Person & person) :
        m_person(person)
    {
    }

    const Person & person() const
    {
        return m_person;
    }

private:
    Person m_person;
};

int main()
{
    Account account(Person("John", "Connor"));
    account.person().setFirstName("Isaac"); // Ошибка
    account.person().setLastName("Asimov"); // Ошибка
    std::cin.get();
    return 0;
}

Строки, помеченные комментарием "Ошибка", к примеру в Visual Studio, приведут к такому сообщению компилятора:

...consoleapplication1.cpp(58): error C2662: 'Person::setFirstName' : cannot convert 'this' pointer from 'const Person' to 'Person &'

На моей практике встречались случаи, когда getter`ы свойств меняли кучу переменных класса, создавая side-эффекты, которые приводили к длительной отладке. Если бы в C# поддерживались константные методы (и getter`ы свойств), то таких ситуаций практически не возникало.

Ссылки

В C++ существует два способа указывания на объект: классические указатели и ссылки. Указатели являются адресом переменной в памяти, а ссылки -- это механизм, введённый в язык C++ с целью повышения удобства и безопасности. Между ссылками и указателями есть две принципиальных разницы:

  1. ссылке нельзя переприсвоить значение;
  2. ссылка не может быть нулевой.

Первое ограничение касается, скорее, только безопасности, а вот второе очень сильно помогает в работе. В C++ принято принимать в функцию указатель, если параметр может быть равным NULL, в противном случае (чаще всего) принимают  ссылку. В C# ссылки могут быть равными null, что приводит к постоянной проверке всех аргументов и частому возникновению NullReferenceException`ов. Да, в C# параметр можно пометить ключевым словом ref, тогда нельзя будет передать null явно, но следующий код будет вполне валидным.

class Program {
    static void PrintString(ref string str) {
        Console.WriteLine(str);
    }

    static void Main(string[] args) {
        string str = null;
        PrintString(ref str);
        Console.ReadKey();
    }
}

Кроме того, использование ссылок в C++ не ограничено только параметрами, их использование допустимо почти всюду, где допустимо использование переменных.

Копирование объектов

Паттерн проектирования prototype (прототип) предполагает наличие одного объекта (прототипа) и его копирования для создания новых объектов. Такой подход часто обусловлен дороговизной инициализации новых объектов. В языке C++ предусмотрены мощные средства для создания копий объектов -- это конструкторы копирования и операторы присваивания. Конструкторы копирования создают новый объект по переданному образцу, а операторы присваивания заполняют существующий объект из образца. C++ никак не ограничивает программиста в реализации упомянутых методов. Если конструктор копирования или оператор присваивания не определены, то компилятор сгенерирует их автоматически. Копирование по умолчанию подразумевает полное побайтовое копирование объектов.

#include <iostream>
#include <cstring>

class Data
{
public:
    Data(void * data, size_t size);
    Data(const Data & data);
    ~Data();
    Data & operator = (const Data & data);
    inline const void * data() const;

private:
    void * mp_data;
    size_t m_size;
};

Data::Data(void * data, size_t size) :
    m_size(size)
{
    mp_data = ::malloc(size);
    ::memcpy(mp_data, data, size);
    std::cout << "Constructor" << std::endl;
}

Data::Data(const Data & data) :
    m_size(data.m_size)
{
    mp_data = ::malloc(m_size);
    ::memcpy(mp_data, data.mp_data, m_size);
    std::cout << "Copy constructor" << std::endl;
}

Data::~Data()
{
    ::free(mp_data);
    std::cout << "Destructor" << std::endl;
}

Data & Data::operator = (const Data & data)
{
    if(this ==  &data) return *this;
    ::free(mp_data);
    m_size = data.m_size;
    mp_data = ::malloc(m_size);
    ::memcpy(mp_data, data.mp_data, m_size);
    std::cout << "Assignment operator" << std::endl;
    return *this;
}

const void * Data::data() const
{
    return mp_data;
}

void test()
{
    char * content = "Hello, World!";

    Data data1(content, strlen(content) + 1); // Конструктор
    Data data2(data1); // Конструктор копирования
    Data data3(NULL, 0); // Конструктор
    data3 = data2; // Оператор присваивания

    std::cout << data1.data() << ": " << static_cast<const char *>(data1.data()) << std::endl;
    std::cout << data2.data() << ": " << static_cast<const char *>(data2.data()) << std::endl;
    std::cout << data3.data() << ": " << static_cast<const char *>(data3.data()) << std::endl;
}

int main()
{
    test();
    std::cin.get();
    return 0;
}
Constructor
Copy constructor
Constructor
Assignment operator
00489448: Hello, World!
0048A5D0: Hello, World!
0048A620: Hello, World!
Destructor
Destructor
Destructor

Из вывода программы видно, что данные действительно скопированы, а конструкторы и оператор присваивания вызваны так, как и ожидалось. Чаще всего, конструкторы копирования и операторы присваивания реализуются для копирования данных, которые хранятся в объекте по указателю, но существуют и другие полезные применения, среди которых:

  • Запрет копирования объектов класса. Реализуется путём объявления конструктора копирования и оператора присваивания в приватной секции. При этом реализацию можно опустить, тогда при попытке использования вы получите ошибку от линкера.
  • Модификация копий объектов или статических переменных. Таким образом, к примеру, реализуются "умные" указатели с подсчётом ссылок.

C# при применении оператора копирования копирует лишь ссылку на объект (за исключением значимых типов данных).  Для копирования объектов предназначен интерфейс ICloneable. Этот интерфейс содержит лишь один метод Clone, который возвращает объект типа object. Из этого следует, что клонированный объект всегда нужно приводить к нужному типу (и надеяться на порядочность разработчиков класса).

class Data : ICloneable {
    private byte[] data;

    public Data(byte[] data) {
        this.data = new byte[data.Length];
        data.CopyTo(this.data, 0);
    }

    public byte[] Buffer {
        get { return data; }
    }

    public object Clone() {
        return new Data(data);
    }
}

unsafe class Program {
    public static void Main(string[] args) {
        byte[] content = Encoding.ASCII.GetBytes("Hello, World");
        Data data1 = new Data(content);
        Data data2 = data1.Clone() as Data;
        PrintData(data1);
        PrintData(data2);
        Console.ReadKey();
    }

    static void PrintData(Data data) {
        fixed(byte* ptr = data.Buffer) {
            Console.WriteLine("{0:x}: {1}", (int)ptr, Encoding.ASCII.GetString(data.Buffer));
        }
    }
}
232f454: Hello, World
232f478: Hello, World

Тем не менее, из примера видно, что подход C# справился с поставленной задачей. Но что будет, если необходимо унаследовать один из классов, к исходникам которого у Вас нет доступа и который не реализует ICloneable?

class Data {
    private byte[] data;

    public void Load(string url) {
        using(WebClient client = new WebClient()) {
            data = client.DownloadData(url);
        }
    }

    public byte[] Buffer {
        get { return data; }
    }
}

class DataChild : Data, ICloneable {
    public object Clone() {
        return new DataChild(); // Не полная копия.
    }
}

В итоге Вы получите частично скопированный объект. Благодаря тому, что C++ гарантирует наличие конструктора копирования и оператора присваивания (за исключением того, когда явно указывается их отсутствие), любой класс может вызвать родительскую реализацию.

Заключение

В этой статье были перечислены лишь самые основные недостатки языка C#. За пределами обзора остались такие темы, как перегрузка операторов, дружественные классы, определение и время жизни переменных и многие другие мелочи. Следует отметить, что C# обладает одной труднореализуемой в C++ возможностью, возможностью применять атрибуты к класам и методам.

Пишем расширение для Mozilla Firefox

В прошлой статье я рассказывал, как написать расширение для браузера Google Chrome. Моя попытка перейти с Mozilla Firefox на Google Chrome, в силу моих личных пристрастий, не увенчалась успехом, поэтому я вернулся на свой любимый браузер. Итак, в этой статье я постараюсь рассказать о том, как написать add-on для Mozilla Firefox.
Для написания дополнений Firefox существуют два подхода: использование XUL и новый, рекомендуемый подход, с использованием легковесного JavaScript SDK.
В этой статье речь пойдёт об использовании второго подхода -- JavaScript SDK. Сразу же приведу ссылки на tutorial`ы и guide`ы, где Вы сможете найти много полезной информации.
По заверениям Mozilla, новые API имеют ряд преимуществ:
  • простота в использовании высокоуровневого API;
  • Mozilla обещает обратную совместимость для всех будущих версий API;
  • тот же API будет использоваться в мобильной версии браузера;
  • повышенная безопасность;
  • использование опыта наработок Mozilla для повышения юзабилити;
  • больше нет необходимости перезапускать браузер после установки дополнения.
Однако, у XUL подхода есть свои собственные плюсы:
  • поддержка локализации;
  • прямой доступ XPCOM;
  • расширяемый пользовательский интерфейс.
Нужно отметить, что SDK поддерживает базовую локализацию, а доступ к XPCOM может осуществляться через низкоуровневый API.
Итак, я надеюсь вернуться к XUL как-нибудь в другой раз, а сейчас приступим к написанию расширения.

Сегодняшнее расширение будет помечать просмотренные видео на youtube`е.

SDK

Для того, чтобы писать расширения для Firefox нам понадобится пакет SDK, который можно скачать здесь. Кроме того, Вы можете воспользоваться онлайн IDE -- Add-on Builder. На видео ниже приведена демонстрация Add-on Builder`а.
Я буду пользоваться скачанным пакетом SDK. После загрузки SDK Вам нужно распаковать архив в удобное место. В архиве, по большому счёту, нас интересует только файл bin/cfx (для пользователей UNIX-подобных ОС) или bincfx.bat для пользователей MS Windows. Я пользуюсь операционной системой Debian GNU/Linux. Я создал символьную ссылку на файл cfx в каталоге /usr/bin, чтобы не писать полный путь к скачанной папке.
Для того, чтобы ознакомиться со справкой программы cfx нужно запустить её без параметров (из консоли). Нас интересует секция
Supported Commands:
docs - view web-based documentation
init - create a sample addon in an empty directory
test - run tests
run - run program
xpi - generate an xpi
Здесь перечислены допустимые команды. Для того, чтобы создать заготовку проекта нужно запустить cfx с параметром init:
$ cfx init
* lib directory created
* data directory created
* test directory created
* doc directory created
* README.md written
* package.json written
* test/test-main.js written
* lib/main.js written
* doc/main.md written

Your sample add-on is now ready.
Do "cfx test" to test it and "cfx run" to try it. Have fun!

В текущем каталоге будет создана структура пакета дополнения:
youtubemarker
├── data
├── doc
│   └── main.md
├── lib
│   └── main.js
├── package.json
├── README.md
└── test
└── test-main.js
Сейчас нас интересуют два файла: package.json и lib/main.js. В первом файле сосредоточена информация о нашем дополнении
{
"name": "youtubemarker",
"fullName": "youtubemarker",
"description": "a basic add-on",
"author": "",
"license": "MPL 2.0",
"version": "0.1"
}
Мы можем вписать свои данные и дополнить объект необходимыми полями. Спецификация объекта приведена здесь.
Поле id будет создано при первом запуске нашего add-on`а.
$ cfx run
No 'id' in package.json: creating a new ID for you.
package.json modified: please re-run 'cfx run'
{
"name": "youtubemark",
"license": "MPL 2.0",
"author": "brainstream",
"version": "0.1",
"fullName": "YouTube Marker",
"id": "jid1-02UvWxeWz56WUA",
"description": "Marks viewed videos"
}
Если при запуске cfx run программа не смогла найти Ваш Firefox, то путь к его исполняемому файлу нужно указать в опции -b, например так:
$ cfx run -b /opt/firefox/firefox
Кроме опции -b нам понадобится использовать опцию -p, которая задаёт профиль, с которым запускается Firefox. Дело в том, что по умолчанию, Firefox запускается каждый раз со временным профилем и вся сохранённая информация теряется. Запустив cfx следующим образом:
$ cfx run -p profile
мы создадим новый профиль в каталоге profile, который будет располагаться в текущем каталоге. Все последующие запуски
$ cfx run -p profile
будут использовать этот профиль. Сюда Вы можете поставить те дополнения, которые Вам нужны для работы с Вашим дополнением, здесь же будут храниться куки и все данные, которые мы сохраним в процессе работы нашего дополнения.

Встраиваемый скрипт

Прейдём непосредственно к коду дополнения. В add-on`ах Firexox используется два вида скриптов: код в дополнении и встраиваемый в страницу код. Различия между ними состоят в доступе к тем или иным API.
Начнём со скрипта, который будет работать на странице youtube`а:
youTubeMarker = {
start: function() {
youTubeMarker.runListing();
youTubeMarker.processPage();
},

runListing: function() {
self.on("message", function(message) {
if(message == null) return;
switch(message.type) {
case "mark":
youTubeMarker.markElement(message.id);
break;
}
});
},

processPage: function() {
if(document.getElementById("watch-video-container") != null) {
youTubeMarker.processWatch();
} else if(document.getElementById("feed") != null) {
youTubeMarker.processPane("feed-item-container", "feed-video-title");
} else if(document.getElementById("browse-main-column") != null) {
youTubeMarker.processPane("browse-item", "yt-uix-sessionlink");
} else if(document.getElementById("search-main") != null) {
youTubeMarker.processPane("result-item-video", "yt-uix-sessionlink");
}
},

processWatch: function() {
self.postMessage({
type: "watch",
url: document.URL
});
},

processPane: function(itemClass, linkClass) {
var items = document.getElementsByClassName(itemClass);
for(var i in items) {
var links = items[i].getElementsByClassName(linkClass);
if(links.length > 0) {
var url = links[0].getAttribute("href");
var id = "youtube-marker-" + youTubeMarker.lastId++;
items[i].id = id;
youTubeMarker.requestMarking(url, id);
}
}
},

lastId: 0,

requestMarking: function(url, id) {
self.postMessage({
type: "mark",
url: url,
id: id
});
},

markElement: function(id) {
var element = document.getElementById(id);
if(element == null) return;
element.style.opacity = 0.4;
}
};

youTubeMarker.start();
Сохраним этот скрипт под именем content.js в каталоге data нашего add-on`а. В этом коде есть всего две особенности, которые выделятся из обычного JavaScript кода: использование методов self.on и self.postMessage. Эти методы используются для коммуникации между скриптом дополнения и встраиваемым скриптом. Метод on принимает первым параметром имя события, а вторым -- обработчик. Метод postMessage отсылает скрипту дополнения JSON объект. К сожалению, более подробной документации по этим методам и объекту self мне найти не удалось.
Итак, скрипт запускается методом start, который организует подписку на сообщения от add-on`а и запускает парсинг страницы. Если страница является страницой просмотра видео, то скрипт отправляет сообщение о том, что видео просмотрено. Если же страница является списком (главная страница, обзор видео или результаты поиска), то для каждого контейнера устанавливается ID и отсылается URL видео с этим ID дополнению с запросом на пометку. Если запрос возвращается, то видео помечается полупрозрачностью.

Основной скрипт

Перейдём к скрипту main.js, основному скрипту дополнения.
var pageMod = require("page-mod");
var data = require("self").data;
var simpleStorage = require("simple-storage");
var querystring = require("api-utils/querystring");

pageMod.PageMod({
include: ["*.youtube.com"],
contentScriptFile: data.url("content.js"),
contentScriptWhen: "end",
onAttach: function(worker) {
worker.on("message", function(message) {
processMessage(worker, message);
});
}
});

function processMessage(worker, message) {
if(message == null) return;
switch(message.type) {
case "mark":
if(!isVideoWatched(message.url)) return;
worker.postMessage({
type: "mark",
id: message.id,
});
return;
case "watch":
saveWatchedUrl(message.url);
return;
}
}

function unifyUrl(url) {
var query = querystring.parse(url.split("?")[1]);
return query.v;
}

function isVideoWatched(url) {
return simpleStorage.storage[unifyUrl(url)] === true;
}

function saveWatchedUrl(url) {
simpleStorage.storage[unifyUrl(url)] = true;
}
Этот файл представляет куда больший интерес. В начале файла мы получаем ссылки на необходимые модули путём вызова функции require. Этой функции передаётся имя модуля высокоуровневого или низкоуровневого API.
Для хранения данных, переданных из встраиваемого скрипта, используется модуль simple-storage, который позволяет хранить данные точно так же, как и localStorage в DOM API. Мы вычленяем из URL идентификатор видео и сохраняем булеву переменную с этим именем в storage.
Объект PageMod из модуля page-mod позволяет запускать скрипты на страницах. Этот объект содержит шаблоны URL (include), на которых следует выполнять инъекцию, имя файла скрипта (contentScriptFile) или сам скрипт (contentScript). Кроме того, можно указать, в какой момент времени нужно запустить скрипт в опции contentScriptWhen, которая может принимать следующие значения:
  • "start": скрипт выполнится сразу же, как элемент документа будет вставлен в DOM;
  • "ready": скрипт будет выполнен после полной загрузки DOM;
  • "end": запуск скрипта произойдёт после загрузки всего контента (DOM, JavaScript`ы, таблицы стилей и картинки).
Так же объект позволяет подписаться на события, которые происходят при аттаче скрипта и при ошибке. В примере приведён первый случай. На обработчике onAttach мы запускаем прослушивание сообщений от встроенного скрипта. В сообщении о подключении скрипта нам приходит объект Page модуля page-worker, через который возможна коммуникация со встроенным скриптом. Здесь нам доступны методы on и postMessage полностью идентичные тем, что мы использовали во встраиваемом скрипте из объекта self.
Адрес скрипта, хранящегося в каталоге data можно получить, использовав объект data из модуля self. Метод data.url() вернёт ссылку, которую мы вставляем в свойство contentScriptFile объекта PageMod.
Низкоуровневый модуль api-utils/querystring позволяет распарсить строку запроса в URL страницы.

Теперь наше дополнение готово к первому запуску. Запустите
$ cfx run -p profile
и посмотрите на youtube.com пару видео с вашего фида или из обзора. Вернитесь на страницу со списком и увидите, что просмотренные видео полупрозрачны.

Добавляем интерактивность

Add-on работает. Теперь нам хочется интерактивности. Давайте добавим пункт в контекстное меню, который будет отображаться при правом щелчке на блок с видео в списках.
Прежде чем приступить к описанию процесса создания меню, необходимо провести небольшой рефакторинг уже написанного кода: вынесем переменную worker из обработчика события onAttach в глобальную область видимости.
var globalWorker;

pageMod.PageMod({
include: ["*.youtube.com"],
contentScriptFile: data.url("content.js"),
contentScriptWhen: "end",
onAttach: function(worker) {
globalWorker = worker;
worker.on("message", function(message) {
processMessage(message);
});
}
});

function processMessage(message) {
if(message == null) return;
switch(message.type) {
case "mark":
if(!isVideoWatched(message.url)) return;
globalWorker.postMessage({
type: "mark",
id: message.id,
});
return;
case "watch":
saveWatchedUrl(message.url);
return;
}
}
Строки, в которые нужно внести правки, выделены полужирным шрифтом.

Теперь добавим пункт меню.
var menu = require("context-menu");
function createMenu() {
var containers = [
"feed-item-container",
"browse-item",
"result-item-video"
];
for(var i in containers) {
menu.Item({
label: "Mark/unmark as viewd",
context: [
menu.URLContext("*.youtube.com"),
menu.SelectorContext("." + containers[i] + " *")
],
data: containers[i],
contentScriptFile: data.url("markmenu.js"),
onMessage: function (message) {
processMenuMessage(message);
}
});
}
}

createMenu();

function processMenuMessage(message) {
if(message == null) return;
switch(message.type) {
case "mark":
saveWatchedUrl(message.url);
break;
case "unmark":
removeWatchedUrl(message.url);
break;
}
globalWorker.postMessage({
type: message.type,
id : message.id,
});
}
Кроме меню, нам понадобится функция для удаления записей из simpleStorage. Для того, чтобы удалить запись из simpleStorage достаточно применить к этой записи оператор delete.
function removeWatchedUrl(url) {
var unifiedUrl = unifyUrl(url);
if(simpleStorage.storage[unifiedUrl] === true) {
delete simpleStorage.storage[unifiedUrl];
}
}
Итак, наш пункт меню будет переключать состояние элемента с просмотренного на не просмотренное и наоборот. Для создания контекстных меню используется модуль context-menu. Мы хотим добавить один пункт меню, поэтому будем использовать объект класса Item. Кроме того, можно добавлять собственные вложенные меню, используя класс Menu. Из-за того, что мы имеем три разных списка, на элементах которых хотим отображать наш пункт меню, нам придётся создать три почти одинаковых объекта. Через свойство label задаётся отображаемый текст. Огромный интерес же, для нас, представляет свойство context. Это свойство задаёт один или более (как в нашем случае) контекст, для которого отображается пункт меню. Для задания контекста должны использоваться следующие функции модуля context-menu:
  • PageContext() не задаёт ограничений, меню будет отображаться на всех элементах страницы;
  • SelectionContext() отобразит меню, если пользователь сделал выделение;
  • SelectorContext(selector) даёт возможность задать CSS селектор тех эелементов, на которых будет отображаться меню;
  • URLContext(matchPattern) задаёт шаблон URL страниц, на которых отображается меню.
В опцию context можно передать либо один из указанных контекстов, либо их массив. Если передать массив контектов, то отображаться меню будет только в случае удовлетворения всем котекстам сразу.
В нашем случае нужно передать два контекста: первый -- контекст на ограничение URL, а второй -- на HTML элементы, которые находятся в контейнерах-элементах списков.
В поле data помещаются данные, которые будут переданы обработчику нажатия меню.
Обработчик onMessage принимает сообщение подобное  тому, что приходит из встраиваемого скрипта, с той лишь разницей, что поле type принимает два значения: "mark" и "unmark".
Для того, чтобы снять метку, нужно немного дописать метод markElement в файле content.js:
markElement: function(id, mark) {
var element = document.getElementById(id);
if(element == null) return;
element.style.opacity = mark === true ? 0.4 : 1.0;
}
Мы добавили флаг того, нужно ли поставить метку или снять её. С той же целью допишем метод runListing
runListing: function() {
self.on("message", function(message) {
if(message == null) return;
switch(message.type) {
case "mark":
youTubeMarker.markElement(message.id, true);
break;
case "unmark":
youTubeMarker.markElement(message.id, false);
break;
}
});
},
При создании пункта меню, в поле contentScriptFile мы указали новый файл -- markmenu.js. Давайте взглянем на него.
youTubeMarkerMarkMenu = {
start: function() {
self.on("click", function(source, data) {
for(var element = source; element != null; element = element.parentElement) {
if(element.className.indexOf(data) < 0) continue;
var url = youTubeMarkerMarkMenu.findUrl(data, element);
if(url == null) return;
self.postMessage({
type: element.style.opacity == 0.4 ? "unmark" : "mark",
id: element.id,
url: url
});
return;
}
});
},

findUrl: function(className, container) {
var linkclass = null;
switch(className) {
case "feed-item-container":
linkclass = "feed-video-title";
break;
case "browse-item":
case "result-item-video":
linkclass = "yt-uix-sessionlink";
break;
default:
return null;
}
var links = container.getElementsByClassName(linkclass);
if(links.length < 1) return null;
return links[0].getAttribute("href");
}
};

youTubeMarkerMarkMenu.start();
Здесь мы видим уже знакомый нам метод self.on. Теперь он принимает сообщение click и два параметра: элемент, на котором произошёл клик и данные, записанные в параметр data при создании элемента меню. В эти данные мы записали класс контейнера, который необходимо найти. С помощью цикла ищем этот контейнер вверх по дереву. Найдя элемент, получаем URL видео и отправляем сообщение основному скрипту. Что происходит дальше Вы уже видели.

Создание пакета

После того, как Ваш add-on закончен, Вы можете создать *.xpi файл для установки его в браузер. Для этого введите команду
$ cfx xpi
и в Вашем рабочем каталоге появится файл с расширением xpi. Для команды
$ cfx xpi
так же можно использовать опцию -b, если cfx не смогла найти Вашего браузера.

Заключение

В этой статье я показал лишь очень малую часть того, что можно сделать с помощью Add-on SDK. Но надеюсь этого будет достаточно для того, чтобы понять общий подход к написанию дополнений для Mozilla Firefox.
Исходные тексты примера можно скачать отсюда.

Пишем расширение для Google Chrome

Как и любой современный web обозреватель, Google Chrome поддерживает расширения. В этой статье я покажу общий подход к написанию extension`ов, после чего Вы без труда напишете свой собственный плагин.
Пример, который я буду показывать, будет называться GoogleMark и его назначением будет метка результатов поиска в google,как показано на скриншоте ниже.
Дабы не утомлять тех, кому достаточно прочесть документацию, приведу сразу на неё ссылку. Там Вы найдёте краткий обзор и руководство по быстрому старту. Но, хочу предостеречь читателя о том, что этих вводных данных не достаточно для комфортной работы. Информация о необходимых мелочах раскидана по всей документации. В этой статье я постараюсь дать ссылки на самые важные части документации.
Итак, расширения для Google Chrome пишутся на JavaScript. Все файлы расширения должны лежать в одном каталоге. Я буду называть этот каталог каталогом расширения.
Для начала я приведу скрипт, который будет раскрашивать страничку результатов поиска google. В нём совсем немного специфичного для расширения кода, по большей части -- это простой JavaScript, который Вы используете при обработке любых web-страниц.
googleMarker = {
preparePage: function() {
var links = document.getElementsByClassName("l");
var count = links.length;
for(var i = 0; i < count; ++i) {
links[i].removeAttribute("onmousedown");
}
},

markUrl: function(url, color) {
var block = googleMarker.findMarkingBlockByUrl(url);
googleMarker.markBlock(block, color);
googleMarker.saveMarker(block, color);
},

restoreUrlMark: function(url, color) {
googleMarker.markBlock(googleMarker.findMarkingBlockByUrl(url), color);
},

findMarkingBlockByUrl: function(url) {
var listItems = document.getElementsByClassName("l");
var itemsCount = listItems.length;
for(var i = 0; i < itemsCount; ++i) {
var item = listItems[i];
if(item.href == url) {
return googleMarker.findMarkingBlockByNestedElement(item);
}
}
return null;
},

findMarkingBlockByNestedElement: function(element) {
if(element == null || element == 'undefined') {
return null;
}
if(element.className == 'r') {
return element;
}
return googleMarker.findMarkingBlockByNestedElement(element.parentNode);
},

markBlock: function(block, color) {
if(block == null || block == 'undefined') {
return;
}
var image = googleMarker.getImage(color);
if(image == null) {
return;
}
block.insertBefore(image, block.firstChild);
},

getImage: function(color) {
var url = null;
switch(color) {
case "green":
url = chrome.extension.getURL("green.png");
break;
case "yellow":
url = chrome.extension.getURL("yellow.png");
break;
case "red":
url = chrome.extension.getURL("red.png");
break;
}
if(url == null) {
return null;
}
var image = new Image();
image.src = url;
return image;
},

saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { color: color, url: url } );
}
}
На случай, если Google поменяет разметку страницы результатов, я приведу HTML одного из блока результатов, которые выдаются сейчас.
<div class="vsc" pved="0CBAQkgowAQ" bved="0CBEQkQo" sig="qmv">
<h3 class="r">
<a href="http://brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html" target="_blank" class="l">
<em>Ещё один блог о программировании</em>: Асинхронное <b>...</b></a>
</h3>
<div class="vspib" aria-label="Подробнее..." role="button" tabindex="0">
<div class="vspii">
<div class="vspiic"></div>
</div>
</div>
<div class="s">
<div class="f kv">
<cite>brainstream-dev.<b>blogspot</b>.com/2012/03/c-visual-studio-11.html</cite>
<span class="vshid">
<a href="http://webcache.googleusercontent.com/search?q=cache:wtBn88UhcVYJ:brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html+&amp;cd=2&amp;hl=ru&amp;ct=clnk&amp;gl=ru"
onmousedown="return rwt(this,'','','','2','AFQjCNHoJc76l2Yol_mH3A94wFMHV1Z5AA','t0iOo1MA-N91FmlNaAEdNQ','0CBMQIDAB',null,event)"
target="_blank">

Сохраненная&nbsp;копия
</a>
</span>
<button class="gbil esw eswd"
onclick="window.gbar&amp;&amp;gbar.pw&amp;&amp;gbar.pw.clk(this)"
onmouseover="window.gbar&amp;&amp;gbar.pw&amp;&amp;gbar.pw.hvr(this,google.time())"
g:entity="http://brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html"
g:undo="poS1" title="Рекомендовать эту страницу"
g:pingback="/gen_204?atyp=i&amp;ct=plusone&amp;cad=S1">

</button>
</div>
<div class="esc slp" id="poS1" style="display:none">
Вы уже поставили +1 этой странице.&nbsp;
<a href="#" class="fl">Отменить</a>
</div>
<span class="st">
<span class="f">4 мар 2012 – </span>
<em>Ещё один блог о программировании</em>.
Все статьи этого блога могут быть использованы в соответствии с лицензией GNU FDL.<br>
</span>
</div>
</div>
Добавлять картинки я буду слева от ссылки в блоке "h3" с классом "l". Теперь разберём скрипт чуть подробнее.
Функция preparePage нужна для того, чтобы убрать все обработчики "onmousedown" из ссылок, иначе ссылка изменится чудесным образом и скрипт её уже никогда не узнает.
markUrl и restoreUrlMark делают одно и то же, за исключением того, что markUrl сохраняет метку (об этом чуть позже).
Функция getImage получает картинки из расширения. Об этом тоже чуть позже.
Все остальные функции являются вспомогательными и просто работают с DOM документа. Сохраним файл со скриптом в каталог расширения под названием googleMarker.js.
Итак, я надеюсь, что с тем, как метки будут появляться на страничке, мы разобрались и я перейду непосредственно к теме статьи.

Манифест

Любое расширение Google Chrome должно содержать файл манифеста, определяющий структуру и права Вашего расширения. Манифест для нашего примера будет следующим:
{
"name": "GoogleMark",
"version": "1.0",
"manifest_version": 2,
"description": "Marks google search results",
"content_scripts": [
{
"matches": [
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"js": [
"googleMarker.js"
]
,
"css": [
"googleMarker.css"
]
}
]
,
"background": {
"page": "background.html"
}
,
"permissions": [
"tabs",
"webNavigation",
"contextMenus",
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"icons": {
"16": "menu-icon-16x16.png"
}
,
"web_accessible_resources": [
"green.png",
"yellow.png",
"red.png"
]
}
Как видно, манифест представляет собой JSON объект. Файл манифеста сохраняется в корневой каталог расширения и должен иметь имя "manifest.json". Разберём структуру объекта примера.
Поля "name", "version", "manifest_version" и "description" понятны без объяснений.
Ваше расширение может встраивать в страницы собственные JavaScript`ы и каскадные таблицы стилей. Всё это описывается в поле "content_scripts", которое представляет собой массив объектов с указанием списка файла JavaScript`ов и файлов CSS. Кроме указания файлов, можно указывать шаблоны URL тех сайтов, в которые данный контент должен быть встроен. Спецификацию шаблонов можно найти в документации.
Каждое расширение может иметь свою теневую страницу. Эта страница загружается при запуске дополнения и может не иметь никакой разметки, кроме подключения JavaScript`ов. Теневая страница указывается в поле "background".
Вы можете явно задать HTML страницу в параметре "page" или, если Вам не нужна страница, то она может быть сгенерирована автоматически, если Вы укажете список скриптов в параметре "scripts".
Права, которыми обладает расширение вообще и теневая страница в частности, перечисляются в поле "permissions". В это поле вносятся шаблоны URL, к которым есть доступ у скриптов и список разрешённых модулей API.
В поле "icons" перечислены иконки расширения разных размеров. В частности, иконка 16x16 будет использоваться в меню (см. скриншот в начале статьи).
В списке "web_accessible_resources" перечисляются те ресурсы дополнения, которые могут быть доступны из страницы браузера. В частности, те скрипты, которые определены в поле "content_scripts" могут получить только те ресурсы, которые здесь перечислены. Функция getImage, в скрипте, приведённом в начале статьи, загружает файлы "green.png", "yellow.png" и "red.png" используя функцию chrome.extension.getURL для получения URL на эти ресурсы. URL ресурсов дополнения выглядит так:
chrome-extension://[ID пакета дополнения]/[путь]
Полную спецификацию манифеста можно найти в документации.

Теневая страница

Непосредственно в теневой странице запрещено исполнение JavaScript политикой безопасности, поэтому код HTML страницы будет представлять из себя только подключение скриптовых файлов и структур HTML для хранения отмеченных страниц. Естественно, что такая организация сохранения высосана из пальца только для того, чтобы продемонстрировать работу с теневой страницей. Вот её разметка.
<html>
<head>
<script type="text/javascript" src="jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="background.js"></script>
</head>
<body>
<ul id="green"></ul>
<ul id="yellow"></ul>
<ul id="red"></ul>
</body>
</html>
Как видно, на теневой странице мы можем использовать любые скрипты, в частности, я подключил jQuery.

Контекстное меню

Для работы с контекстным меню в Google Chrome API служет модуль contextMenus. Для его работы нужно добавить строку "contextMenus" в поле "permissions" манифеста. Добавим в файл background.js функцию для создания меню. Внимание, большинство API функций Google Chrome являются асинхронными и возвращают управление сразу после вызова. Для обработки результатов все асинхронные вызовы опционально принимают функцию обратного вызова.
function createMenu() {
var urls = [
"*://*.google.com/*",
"*://*.google.ru/*"
];

var root = chrome.contextMenus.create({
title: "Mark as",
contexts: [ "link" ],
documentUrlPatterns: urls
});

chrome.contextMenus.create({
title: "Green",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'green')"
});
}
});

chrome.contextMenus.create({
title: "Yellow",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'yellow')"
});
}
});

chrome.contextMenus.create({
title: "Red",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'red')"
});
}
});
}

createMenu();
Всё элементарно. Вызывая функцию chrome.contextMenus.create и передавая в неё детальное описание, мы получаем новый пункт меню. Полное описание объекта, описывающего пункт меню можно посмотреть в документации к функции, а я опишу лишь самые основные.
В параметре documentUrlPatterns перечисляются шаблоны URL адресов, на которых пункт меню будет появляться.
В списке contexts перечисляются типы элементов, при правом щелчке на которые, будет появляться данный пункт меню. Доступные варианты такие: "all", "page", "frame", "selection", "link", "editable", "image", "video", "audio".
Параметр parentId является дескриптором родительского пункта меню. Каждый вызов функции chrome.contextMenus.create возвращает такой дескриптор.
Наиболее интересным, для нас, является поле onclick. Сюда помещается обработчик выбора пункта меню.

Вкладки

В функцию обратного вызова onclick элемента меню, помимо информации об элементе, для которого было вызвано контекстное меню, передаётся информация о вкладке, в которой отображается документ с указанным элементом.
Одной из самых полезных функций модуля tabs (не забудьте указать значение "tabs" в поле "permissions", в манифесте) является функция chrome.tabs.executeScript. Как ясно из названия, она позволяет выполнить скрипт в документе, который находится во вкладке, идентификатор которой передаётся первым параметром. Вторым параметром передаётся объект, содержащий код скрипта или имя файла со скриптом. Работу этой функции и демонстрируют обработчики onclick пунктов меню.

Запросы

Для синхронизации работы встраиваемых скриптов с теневой страницей Google Chrome API предоставляют возможность скриптам обмениваться информацией посредством запросов. Напомню, как выглядит функция сохранения метки в файле googleMarker.js
saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { color: color, url: url } );
}
В этой функции отсылается запрос с объектом, описывающим маркер для сохранения. Вообще говоря, Вы можете передавать любой объект в качестве запроса в функцию chrome.extension.sendRequest. Кроме того, этой функции можно передать идентификатор дополнения и функцию прослушки ответов.
Для работы с функциями из модуля extension не требуется никаких указаний в манифесте.
Чтобы наше расширение смогло реагировать на запросы, следует подписаться на их прослушивание. В нашем случае это будет выгладить так:
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
var $list = $("#" + request.color);
if($list.lengh == 0) {
return;
}
$list.append($("<li>").append(request.url));
}
);
Параметры функции обработки события chrome.extension.onRequest довольно очевидны; это запрос, отправитель запроса (идентификаторы вкладки и дополнения) и функция отправки ответа.
В нашем случае, мы, воспользовавшись jQuery, добавляем в теневую страницу новый элемент списка.

События навигации

Google Chrome API предоставляют возможность отслеживать события навигации с помощью модуля webNavigation. Для его использования следует добавить в Ваши "permissions" элемент "webNavigation" в файле манифеста. Модуль предоставляет несколько полезных событий и функций, я же продемонстрирую использование события chrome.webNavigation.onDOMContentLoaded, наступающего, как и следует из названия, после загрузки дерева элементов документа. Мы воспользуемся этим событием для подготовки страницы (удаления атрибута onmousedown) и восстановления сохранённых меток.
chrome.webNavigation.onDOMContentLoaded.addListener(
function(details) {
if(details.frameId != 0) {
return;
}
chrome.tabs.executeScript(details.tabId, {
code: "googleMarker.preparePage()"
}
);
var colors = ["green", "yellow", "red"];
for(var i = 0; i < colors.length; ++i) {
var $urls = $("#" + colors[i] + " li");
if($urls.length == 0) {
continue;
}

$urls.each(function() {
chrome.tabs.executeScript(details.tabId, {
code: "googleMarker.restoreUrlMark('" +
this.innerText + "', '" + colors[i] + "')"
}
);
});
}
}
);
В функцию обработки события приходит объект, содержащий идентификатор вкладки, идентификатор фрейма, URL и время окончания загрузки дерева. Идентификатор фрейма равен нулю, если дерево было построено в корневом документе вкладки, иначе -- это уникальный (в пределах вкладки) идентификатор фрейма HTML страницы. Всё остальное из фрагмента кода, приведённого чуть выше, нам уже знакомо.

Завершение

Последнее, что нам осталось сделать -- это добавить файлы googleMarker.css, menu-icon-16x16.png, green.png, yellow.png и red.png для отображения всех необходимых элементов.
.r img {
margin-right: 15px;
}
Теперь плагин готов к установке. Для этого переходим в Google Chrome в меню -> Tools -> Extesions и ставим галку на Developer mode. Нажимаем Load unpacked extension и выбираем каталог с расширением.
Теперь мы можем протестировать новое расширение и отладить его. Все встраиваемые скрипты доступны из стандартного инструментария (F12), а чтобы отладить скрипты теневой страницы достаточно нажать на соответствующую ссылку в менеджере дополнений.

Иконка и popup

Итак, убедившись, что всё работает, мы хотим добавить немного интерактивности в виде иконки расширения и всплывающего окна с кнопкой для очистки всех маркеров.
Для этого добавляем файл иконки (icon.png) и два файла для представления popup окна: popup.html и popup.js. Заносим новые сведения в файл манифеста, добавляя поле browser_action
{
"name": "GoogleMark",
"version": "1.0",
"manifest_version": 2,
"description": "Marks google search results",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
}
,

"content_scripts": [
{
"matches": [
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"js": [
"googleMarker.js"
]
,
"css": [
"googleMarker.css"
]
}
]
,
"background": {
"page": "background.html"
}
,
"permissions": [
"tabs",
"webNavigation",
"contextMenus",
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"icons": {
"16": "menu-icon-16x16.png"
}
,
"web_accessible_resources": [
"green.png",
"yellow.png",
"red.png"
]
}
Разметка popup.html очень проста
<html>
<body>
<button id="clear-btn">Clear markers</button>
<script src="popup.js" type="text/javascript"></script>
</body>
</html>
Здесь только кнопка и подключение скрипта. Выполнение inline скриптов снова отключено по соображениям безопасности.
В файле popup.js мы подписываемся на нажатие кнопки и делаем запрос к фоновой странице.
var button = document.getElementById("clear-btn");
button.addEventListener("click", function() {
chrome.extension.sendRequest( { action: "clear" } );
});
Так как у нас появляется второе событие, то вводим поле action в запрос. По этой причине дописываем значение "save" в запросе на сохранение маркера (файл googleMarker.js)
saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { action: "save", color: color, url: url } );
}
Осталось немного поправить обработчик запросов:
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
switch(request.action) {
case "save":
var $list = $("#" + request.color);
if($list.lengh == 0) {
return;
}
$list.append($("<li>").append(request.url));
break;
case "clear":
$("ul").empty();
break;
}
}
);
Всё готово! Жмём кнопку Reload в менеджере расширений и у нас появляется значок в верхнем правом углу браузера. Теперь Вы можете упаковать ваше расширение в файл *.crx нажав кнопку Pack extestion в менеджере расширений или залить его в Chrome Web Store.
Скачать исходный код примера можно здесь.

Асинхронное программирование на C# в Visual Studio 11

На днях я побывал на конференции Windows 8 Camp и увидел немало интересных вещей. Среди них была продемонстрирована новая концепция асинхронного программирования на C#.
Для написания примеров этой статьи я использовал Visual Studio 11 Developer Preview.
Итак, в C#, входящем в состав Visual Studio 11, появились два новых ключевых слова: async и await. Хотелось бы продемонстрировать их работу сразу на примере.
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncDemo {
class Program {
const string url = "http://download.mozilla.org/?product=firefox-10.0.2&os=win&lang=en-US";
const string filename = "C:\\firefix.exe";

static void Main(string[] args) {
Test();
while(true) {
Console.Write("-");
Console.Out.Flush();
Thread.Sleep(20);
}
}

static async void Test() {
using(WebClient webClient = new WebClient()) {
Task<byte[]> downloadTask = webClient.DownloadDataTaskAsync(url);
byte[] file = await downloadTask;
Console.WriteLine("Downloaded");
using(FileStream stream = File.OpenWrite(filename)) {
Task writeTask = stream.WriteAsync(file, 0, file.Length);
await writeTask;
Console.WriteLine("Written");
}
}
}
}
}
Как видно из примера, кроме новых ключевых слов, в .NET 4.5 появились новые методы. Все методы, поддерживающие асинхронное выполнение, в .NET 4.5 оканчиваются на Async.
Метод, содержащий асинхронное выполнение, должен быть помечен ключевым словом async и делится на две части: до ключевого слова await и после него.
Методы, поддерживающие асинхронное выполнение, должны возвращать значение типа Task или Task<TResult>. Применение операции awiat к объекту Task будет отложено до тех пор, пока задача не будет выполнена. После начала ожидания управление возвращается в рабочий поток, а обработчик будет вызван в другом потоке. Вторая часть метода (после ключевого слова await) и является, по сути, обработчиком события завершения асинхронной операции.
Вывод примера будет следующим:
Как видите, вывод основного потока вклинился между окончанием операции загрузки и завершением сохранения файла.
Нет ничего сложного в написании асинхронных методов самостоятельно. Для этого Вам всего лишь нужно вернуть из Вашего метода значение типа Task или Task<TResult>.  В конструктор такого объекта следует передать делегат, который будет выполняться. Далее, нужно запустить задачу вызовом метода Start и вернуть созданный объект.
using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncDemo {
class Program {
static void Main(string[] args) {
Test2();
while(true) {
Console.Write("-");
Console.Out.Flush();
Thread.Sleep(20);
}
}

static async void Test2() {
Task<string> task = new AsyncWorker().WorkAsync();
string message = await task;
Console.WriteLine(message);
}
}

class AsyncWorker {
public Task<string> WorkAsync() {
Task<string> task = new Task<string>(Worker);
task.Start();
return task;
}

string Worker() {
Console.WriteLine("Step 1");
Thread.Sleep(500);
Console.WriteLine("Step 2");
Thread.Sleep(500);
return "Done";
}
}
}
Класс Task содержит некоторое количество интересных статических методов. Ознакомиться с ними можно в документации msdn.

Отслеживание событий тестирования NUnit

Работая с фреймворком для unit-тестирования .NET сборок NUnit, Вы не можете получить доступ к событиям тестирования из Ваших фикстур (Fixture) напрямую. Для отслеживания событий Вы должны написать add-in. В этой статье я приведу простой пример того, как это можно сделать.
Создадим простую библиотеку классов, которую будем тестировать.
using System;

namespace TestedLibrary {
public class Engine {
uint cylinderCount = 1;
float capacity = 0.0f;
float power = 0.0f;

public uint CylinderCount {
get {
return cylinderCount;
}

set {
if(value < 1) {
throw new Exception("Cylinder count must be great then 0");
}
cylinderCount = value;
}
}

public float Capacity {
get {
return capacity;
}

set {
if(value < 0.0) {
throw new Exception("Engine capacity must be great then 0.0");
}
capacity = value;
}
}

public float Power {
get {
return power;
}

set {
if(value < 0.0) {
throw new Exception("Engine power must be great then 0.0");
}
power = value;
}
}
}
}
namespace TestedLibrary {
public class Car {
public Car(string name, Engine engine) {
Name = name;
Engine = engine;
}

public string Name { get; set; }

public Engine Engine { get; set; }
}
}
Создадим проект для тестирования и добавим в него пару фикстур:
using NUnit.Framework;
using TestedLibrary;

namespace Testing {
[TestFixture]
public class EngineTest {
[Test]
public void DefaultValues() {
Engine engine = new Engine();
Assert.AreEqual(0.0f, engine.Power, 0.1f);
Assert.AreEqual(0.0f, engine.Capacity, 0.1f);
Assert.AreEqual(1u, engine.CylinderCount);
}

[Test]
public void ManuallySettedValues() {
Engine engine = new Engine() {
Capacity = 1995.0f,
CylinderCount = 4u,
Power = 135000.0f
};
Assert.AreEqual(135000.0f, engine.Power, 0.1f);
Assert.AreEqual(1995.0f, engine.Capacity, 0.1f);
Assert.AreEqual(4u, engine.CylinderCount);
}
}
}
using NUnit.Framework;
using TestedLibrary;

namespace Testing {
[TestFixture]
public class CarTest {
[Test]
public void Creation() {
Engine engine = new Engine() {
Capacity = 1995.0f,
CylinderCount = 4u,
Power = 135000.0f
};
Car car = new Car("BMW Х3 xDrive20d Urban", engine);
Assert.AreSame(engine, car.Engine);
Assert.AreEqual("BMW Х3 xDrive20d Urban", car.Name);
}
}
}
Теперь можем скомпилировать и запустить unit-тесты для нашей библиотеки
Как я уже говорил, чтобы добраться до событий тестирования, нужно создать add-in для NUnit. Для этого нужно создать новый проект C# из шаблона "Class Library" и добавить в него ссылки на сборки nunit.core.dll и nunit.core.interfaces.dll. По умолчанию они находятся в каталоге C:Program Files (x86)NUnit 2.5.10binnet-2.0lib.
При запуске NUnit просматривает каталог addins, находящийся рядом с исполняемым файлом, и загружает из всех сборок классы с атрибутом NUnit.Core.Extensibility.NUnitAddinAttribute. Кроме того, эти классы должны реализовывать интерфейс NUnit.Core.Extensibility.IAddin, содержащий всего один метод:
namespace NUnit.Core.Extensibility {
public interface IAddin {
bool Install(IExtensionHost host);
}
}
Для того, чтобы подписаться на прослушивание событий, мы должны реализовать интерфейс NUnit.Core.EventListener и передать объект этого интерфейса в метод Install объекта типа NUnit.Core.Extensibility.IExtensionPoint, который нужно получить следующим образом:
IExtensionPoint listeners = host.GetExtensionPoint("EventListeners");
Наш класс прослушивания будет называться NUnitReporter и его целью будет создание отчёта в формате HTML. В качестве параметра конструктор этого класса будет принемать имя выходного файл. С учётом всего выше сказанного, полный код класса-расширения будет следующим:
using NUnit.Core.Extensibility;

namespace NUnitReport {
[NUnitAddin]
public class Addin : IAddin {
public bool Install(IExtensionHost host) {
IExtensionPoint listeners = host.GetExtensionPoint("EventListeners");
listeners.Install(new NUnitReporter(@"D:report.html"));
return true;
}
}
}
Интерфейс NUnit.Core.EventListener выглядит следующим образом:
namespace NUnit.Core {
public interface EventListener {
void RunFinished(Exception exception);
void RunFinished(TestResult result);
void RunStarted(string name, int testCount);
void SuiteFinished(TestResult result);
void SuiteStarted(TestName testName);
void TestFinished(TestResult result);
void TestOutput(TestOutput testOutput);
void TestStarted(TestName testName);
void UnhandledException(Exception exception);
}
}
Методы RunStarted и RunFinished вызываются при старте и завершении тестирования. В качестве аргумента методу RunFinished передаются результаты всего тестирования или объект возникшего исключения. SuiteStarted и SuiteFinished вызываются для всех сущностей шире единичного теста: класс, пространство имён, Run. Методы TestStarted и TestFinished будут вызваны при запуске и завершении каждого теста. Наконец, метод UnhendledException вызывается, как ясно из названия, при возникновении необработанного исключения.
Приведу простейшую реализацию класса NUnitReporter, собирающего информацию в файл HTML.
using System;
using System.Collections;
using System.IO;
using System.Text;
using NUnit.Core;

namespace NUnitReport {
public class NUnitReporter : EventListener {
string outFileName;
StringBuilder html;

public NUnitReporter(string outFileName) {
this.outFileName = outFileName;
}

public void RunFinished(System.Exception exception) {
}

public void RunFinished(TestResult result) {
PrintRunResult(result);
using(FileStream stream = File.OpenWrite(outFileName)) {
byte[] content = Encoding.UTF8.GetBytes(html.ToString());
stream.Write(content, 0, content.Length);
}
}

void PrintRunResult(TestResult result) {
html = new StringBuilder(
"<html>rn" +
"<head>rn" +
"<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>rn" +
"<title>NUnit Report</title>rn" +
"</head>rn" +
"<body>rn" +
"<div id='run'>rn");
html.AppendLine(String.Format("<h1>{0}</h1>", result.Name));
PrintSuiteResults(result.Results);
html.AppendLine(
"</div>rn" +
"</body>rn" +
"</html>");
}

void PrintSuiteResults(IList results) {
foreach(TestResult result in results) {
html.AppendLine("<div class='suite'>");
html.AppendLine(String.Format("<h2>{0}</h2>", result.Name));
PrintClassResults(result.Results);
html.AppendLine("</div>");
}
}

void PrintClassResults(IList results) {
foreach(TestResult result in results) {
html.AppendLine("<div class='class'>");
html.AppendLine(String.Format("<h3>{0}</h3>", result.Name));
PrintTestResults(result.Results);
html.AppendLine("</div>");
}
}

void PrintTestResults(IList results) {
foreach(TestResult result in results) {
string stringResult = "";
if(result.IsSuccess) stringResult = "Success";
else if(result.IsFailure) stringResult = "Failure";
else if(result.IsError) stringResult = "Error";
html.AppendLine("<div class='test'>");
html.AppendLine(String.Format("<p>{0}: {1}</p>", result.Name, stringResult));
html.AppendLine("</div>");
}
}

public void RunStarted(string name, int testCount) {
}

public void SuiteFinished(TestResult result) {
}

public void SuiteStarted(TestName testName) {
}

public void TestFinished(TestResult result) {
}

public void TestOutput(TestOutput testOutput) {
}

public void TestStarted(TestName testName) {
}

public void UnhandledException(System.Exception exception) {
}
}
}
Больший интерес во всём примере представляет объект класса NUnit.Core.TestResult. Этот объект содержит информацию о результатах только что завершившегося теста и всех вложенных. К примеру, в результат тестирования пространства имён будут вложены результаты тестирования всех его классов. Эти результаты помещаются в список NUnit.Core.TestResult.Results.
Кроме того, класс NUnit.Core.TestResult имеет множетсво интересных свойств:
public int AssertCount { get; set; }
public string Description { get; }
public bool Executed { get; }
public FailureSite FailureSite { get; }
public virtual string FullName { get; }
public bool HasResults { get; }
public virtual bool IsError { get; }
public virtual bool IsFailure { get; }
public virtual bool IsSuccess { get; }
public string Message { get; }
public virtual string Name { get; }
public IList Results { get; }
public ResultState ResultState { get; }
public virtual string StackTrace { get; set; }
public ITest Test { get; }
public double Time { get; set; }
Файл, содержащий класс Addin следует положить в каталог addins, рядом с исполняемым файлом nunit.exe. Если такого каталога не существует, его нужно создать. Для отладки библиотеки следует аттачиться к процессу nunit-agent.exe (Debug->Attach to Process).
Полученный отчёт будет выгладить так:
Естественно, что приведённый пример сильно упрощён и не годится для реального использования, но, я надеюсь, демонстрирует общий подход.
Исходные тексты проекта можно скачать здесь.

Краткое введение в eXpress Application Framework

Часто тут и там мы слышим о концепции отделения бизнес-логики приложения от UI. Разные софтверные компании предлагают разные решения этой задачи. Microsoft, к примеру, продвигает технологию WPF, а для Qt разрабатывается QML. В этой статье я хочу познакомит Вас с решением от компании Developer Express -- eXpress Application Framework (или просто XAF). Фреймворк предоставляет невероятно много функционала, поэтому я расскажу только о вершине айсберга. Возможно, в следующих статьях, я расскажу больше.
Сразу же стоит отметить, что XAF -- это фреймворк для .NET. Поэтому, если Вы хотите написать кроссплатформенное приложение, то этот фреймворк не для Вас. Под Mono XAF тоже не работает.
Помимо минусов, у XAF есть и положительные качества. К примеру, XAF реализован для WinForms и ASP.NET приложений таким образом, что Вам не нужно задумываться, для какой платформы Вы пишите, конечный продукт будет работать на обеих платформах.
Для того, чтобы начать программировать с использованием XAF не нужно много знать о его устройстве, достатьчно прочесть tutorial на официальном сайте продукта. После установки фреймворка на компьютер, в Visual Studio появляются мастера создания проектов. Вы можете создать проект для WinForms, для ASP.NET или для обеих платформ сразу. Приложения XAF имеют модульную архитектуру.  Мастер сгенерирует несколько проектов, один из которых будет являться общим для всех приложений модулем. Также будут созданы модули для win и web приложений отдельно.
Все эти шаги подробно описаны в указанном tutorial. В этой же статье я приведу пример создания XAF приложения полностью вручную. Скачать демо версию фреймворка Вы можете с официально сайта.

База данных

Для работы приложения XAF Вам понадобится база данных на одной из поддерживаемых СУБД. В приведённом примере я буду использовать MySQL. Придумайте название для Вашего приложения и создайте базу данных с этим именем. Настоятельно рекомендую использовать кодировку UTF-8 для вновь созданной базы MySQL. Мой пример будет носить имя "XafDemo".

Создание базового приложения

Теперь, когда мы обзавелись базой данных, можно приступать к созданию приложения. Для этого создадим в студии каркас Windows Form Application и выкинем из него всё лишнее, оставив только файл Program.cs и ссылку на модуль System. Теперь добавим нужные нам ссылки:
  • System.configuration
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.Images.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1
  • DevExpress.Xpo.v11.1.Providers
  • MySql.Data
Последняя ссылка требует установленного MySQL .NET Connector'а и зависит от той СУБД, которую Вы используете.
Добавим к проекту файл конфигурации и пропишем в нём Connection String до базы данных. Мой конфигурационный файл будет выглядеть так:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="ConnectionString" value="XpoProvider=MySql;server=192.168.1.2;user id=sa; password=123456; database=XafDemo;persist security info=true;CharSet=utf8;"/>
</appSettings>
</configuration>

Модуль бизнес-логики

Теперь нужно создать ещё один проект, в котором будет содержаться бизнес-логика нашего приложения. Этот проект должен быть библиотекой классов. Назовём его DemoModule. Так же, как и в первом случае, выкидываем из вновь созданного модуля всё лишнее. Вот те ссылки, которые должны быть в проекте:
  • System
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1
Чтобы XAF заметил наш модуль, мы должны реализовать публичный класс, унаследованный от DevExpress.ExpressApp.ModuleBase. В нашем случае, класс тривиален:
using DevExpress.ExpressApp;

namespace DemoModule
{
public class DemoModule :
ModuleBase
{
}
}

Реализация базового приложения

Теперь у нас есть модуль, который мы можем зарегистрировать в базовом приложении. Возвращаемся в проект XafDemo, добавляем в зависимости проект DemoModule и прописываем его в файле конфигурации
<add key="Modules" value="DemoModule"/>
Для запуска приложения требуется создать экземпляр класса DevExpress.ExpressApp.WinApplication. Обернём создание экземпляра этого класса в класс XafDemoApplication.
using DevExpress.ExpressApp.Win;
using System.Configuration;
using System;
using DevExpress.ExpressApp.Updating;

namespace XafDemo
{
public class XafDemoApplication : IDisposable
{
private WinApplication application;

public XafDemoApplication(string[] arguments)
{
Init();
}

private void Init()
{
application = new WinApplication()
{
Title = "XAF Demo Application"
};
application.Setup("XafDemo", LoadConnectionString(), LoadModules());
}

private string LoadConnectionString()
{
return ConfigurationManager.AppSettings["ConnectionString"];
}

private string[] LoadModules()
{
string modules = ConfigurationManager.AppSettings["Modules"];
if(String.IsNullOrEmpty(modules))
return new string[0];
return modules.Split(';');
}

public void Start()
{
application.Start();
}

public void HandleException(Exception exception)
{
application.HandleException(exception);
}

public void Dispose()
{
application.Dispose();
}
}
}
Метод Setup класса WinApplication, в этом примере, принимает три аргумента: имя приложения (соответствует имени базы данных), connection string к базе данных и загружаемые модули. Метод HandleException показывает удобный Message Box с подробным сообщением об ошибке.
Чтобы запустить приложение, достаточно простой реализации метода Main.
using System;

namespace XafDemo
{
public class XafDemo
{
[STAThread]
public static void Main(string[] args)
{
using(XafDemoApplication application = new XafDemoApplication(args))
{
try
{
application.Start();
}
catch (Exception err)
{
application.HandleException(err);
}
}
}
}
}

Однако, при попытке запустить приложение, окажется, что структура базы данных не созана и приложение завершиться с ошибкой. Для того, чтобы база данных была в актуальном состоянии, её следует постоянно обновлять. Добавим метод для обновления базы в класс XafDemoApplication.
public void UpdateDatabase()
{
DatabaseUpdater updater = application.CreateDatabaseUpdater();
updater.Update();
}
Выполнив этот метод, мы поместим всю служебную информацию в базу. Кроме того, будут созданы таблицы для хранения всех бизнес-объектов, о которых чуть позже.

Database updater

Было бы неосмотрительно давать каждому пользователю возможность обновлять базу, поэтому вынесем функционал для обновления в отдельный проект. Для этого создадим Console Application и, по традиции, выкинем всё лишнее из сгенерированного проекта. Назовём новый проект DBUpdater. Понадобятся нам лишь те библиотеки, которые мы использовали в модуле и сам модуль:
  • System
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1 
  • DemoModule
Скопируем файл конфигурации из XafDemo в проект и реализуем метод Main.
using System;
using XafDemo;

namespace DBUpdater
{
public class DBUpdater
{
static void Main(string[] args)
{
using(XafDemoApplication application =
new XafDemoApplication(args))
{
try
{
Console.Write("Updating: ");
application.UpdateDatabase();
Console.WriteLine("success!");
}
catch(Exception err)
{
Console.WriteLine("fail!");
Console.WriteLine("Error details:");
Console.WriteLine("Type: {0}", err.GetType().FullName);
Console.WriteLine("Message: {0}", err.Message);
Console.WriteLine("Stack trace:");
Console.WriteLine(err.StackTrace);
Console.WriteLine();
}
Console.Write("Press any key...");
Console.ReadKey();
}
}
}
}
Запустив это приложение, мы создадим рабочую структуру базы. Но есть одна, не очень приятная, особенность. При выполнении метода Setup XAF показывает splash окно. А в консольном приложении оно выглядит глупо. Добавим в XafDemoApplication функционал для отключения splash. Сделать это можно установив null в свойство SplashScreen класса WinApplication. Добавим конструктор, принимающий флаг того, нужно ли отключать окно приветствия, а в методе Init выполним проверку этого флага и установим null в SplashScreen.
public XafDemoApplication(string[] arguments)
{
Init(false);
}

public XafDemoApplication(string[] arguments, bool suppressSplash)
{
Init(suppressSplash);
}

private void Init(bool suppressSplash)
{
application = new WinApplication()
{
Title = "XAF Demo Application"
};
if(suppressSplash)
application.SplashScreen = null;
application.Setup("XafDemo", LoadConnectionString(), LoadModules());
}
Изменим создание XafDemoApplication в DBUpdater, передав true вторым аргументом и противное окно больше не появится.

Первый запуск

Всё готово к первому запуску. Так как мы не реализовали ни одного бизнес-объекта и не прилинковали ни одного стандартного модуля, то окно приложения будет абсолютно пустым

Бизнес-объекты

Чтобы в нашем окне появилось что-нибудь интересное, нужно это создать. Все бизнес объекты, с которыми работает XAF являются объектами ORM eXpress Persistent Objects (XPO), разрабатываемой той же Developer Express. В качестве базового класса для всех бизнес-объектов удобно использовать класс DevExpress.Xpo.XPObject.
Создадим класс Employee, описывающий сотрудника некой фирмы.
using System;
using DevExpress.Xpo;

namespace DemoModule
{
public class Employee : XPObject
{
private string firstName;
private string secondName;

public Employee(Session session) :
base(session)
{
}

public string FirstName
{
get { return firstName; }
set { SetPropertyValue<string>("FirstName", ref firstName, value); }
}

public string SecondName
{
get { return secondName; }
set { SetPropertyValue<string>("SecondName", ref secondName, value); }
}
}
}
Как видно, нет ничего сложного. Все открытые свойства, по умолчанию, сохраняются в базе и отображаются на форме. Конструктор класса XPObject принимает объект Session, который создаётся при установлении соединения с базой данных. Чтобы поместить новое значение свойства в базу, следует использовать метод SetPropertyValue, объявленный в классе DevExpress.Xpo.PersistentBase.
Чтобы обновить схему в базе данных, запустите DBUpdater.
После запуска XafDemoApplication мы не увидим ни каких изменений. Это случилось потому, что мы не указали XAF, что хотим видеть.

Модель XAF

Для того, чтобы указать XAF, что мы хотим видеть на форме, мы должны создать файл настроек модели. Добавьте простой текстовый файл с именем Model.DesignedDiffs.xafml в проект DemoModule и установите его свойство "Build Action" в "Embedded Resource". Запишите в этот файл следующий текст
<?xml version="1.0" encoding="utf-8"?>
<Application/>
После пересборки проекта этот файл можно использовать для настройки UI. Делается это либо через встроенный в Visual Studio редактор, либо вручную, запустив программу DevExpress.ExpressApp.ModelEditor.v11.1.exe, которая лежит, у меня, по адресу c:Program FilesDevExpress 2011.1eXpressApp FrameworkToolsModel Editor. В качестве опций эта программа принимает путь до модуля (файла *.dll) и путь до каталога с файлом Model.DesignedDiffs.xafml.
Итак, запустив редактор, мы видим следующее окно

Переходим в раздел NavigationItems и добавляем в пункт Items новый NavigationItem, щёлкнув ПКМ. Это будет наш корневой раздел, назовём его "Organization" (впишите имя в поле Id). Добавим во вновь созданный NavigationItem новый NavigationItem и выберем из списка "View" пункт "Employee_ListView". В полях "Id" и "Caption" выставим имя "Employees". В поле ImageName можно выбрать картинку, которая будет отображаться в пункте навигации. Сохраняем настройки и выходим. После прекомпиляции, в нашем окне добавится новая информация.

Нажав на кнопку "new" мы можем создать новый объект класса Employee.
Можно заметить "лишнее" поле "Oid", которое XPO использует для идентификации объектов в базе данных. Естественным желанием будет удалить это поле. Для этого открываем редактор модели и переходим в пункт Views/DemoModule/Employee_DetailView/Layout/Main/SimpleEditors и удаляем пункт XPObject.

Наложение ограничений на свойства

Наша программа позволяет создать объект класса Employee без имени и фамилии. Таких людей в базе мы видеть не хотим. XAF совместно с XPO позволяют наложить ограничения на поля бизнес-классов простым добавлением атрибутов. Чтобы использовать систему валидации нужно добавить две зависимости: DevExpress.Persistent.Base.v11.1 и DevExpress.ExpressApp.Validation.v11.1. В последнем модуле находится XAF модуль DevExpress.ExpressApp.Validation.ValidationModule. Мы должны добавить зависимость от него в наш модуль DemoModule. Для этого в конструктор DemoModule пишем строку
RequiredModuleTypes.Add(typeof(ValidationModule));
Теперь мы можем наложить атрибут DevExpress.Persistent.Validation.RuleRequiredFieldAttribute на поля FirstName и SecondName. Полученный код будет выглядеть так:
[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.FirstName",
DefaultContexts.Save,
"First Name cannot be empty")]
public string FirstName
{
get { return firstName; }
set { SetPropertyValue<string>("FirstName", ref firstName, value); }
}

[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.SecondName",
DefaultContexts.Save,
"Second Name cannot be empty")]
public string SecondName
{
get { return secondName; }
set { SetPropertyValue<string>("SecondName", ref secondName, value); }
}
После перекомпиляции проекта и обновления базы при попытке сохранить объект без указания имени и фамилии сотрудника мы получим соответствующую ошибку.
Первым параметром атрибута мы указываем уникальный идентификатор валидатора, вторым аргументом указываем, когда нужно запускать проверку. В последнем параметре указывается сообщение, отображаемое в сообщении об ошибке.
Кроме автоматической валидации, модуль DevExpress.ExpressApp.Validation создаёт кнопку "Validate" в тулбаре окна (зелёная галочка), нажав на которую мы можем принудительно провести валидацию.

Отношение ассоциации "один ко многим"

Очень часто возникает ситуация, когда один объект хранит коллекцию других объектов. Фреймворк XPO позволяет создать такое отношение в базе данных, а XAF обеспечит полноценное отображение и удобную работу с такими ассоциациями.
Давайте создадим класс Position и присвоим каждому сотруднику ссылку на объект этого класса. Каждый сотрудник может занимать только одну должность, в то время, как должность содержит список всех сотрудников, которые её занимают.
using System;
using DevExpress.Xpo;
using DevExpress.Persistent.Validation;

namespace DemoModule
{
public class Position :
XPObject
{
private string name;

public Position(Session session) :
base(session)
{
}

[RuleUniqueValue(
"RuleUniqueValue for DemoModule.Position.Name",
DefaultContexts.Save,
"Name must be unique")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Position.Name",
DefaultContexts.Save,
"Name cannot be empty")]
public string Name
{
get { return name; }
set { SetPropertyValue<string>("Name", ref name, value); }
}

[Association("Employee-Position")]
public XPCollection<Employee> Employees
{
get { return GetCollection<Employee>("Employees"); }
}
}
}
Обратите внимание, для гарантирования уникальности имени продукта, я применил атрибут RuleUniqueValue.
Для хранения коллекции объектов в базе данных используется тип DevExpress.Xpo.XPCollection. Чтобы реализовать ассоциацию "один ко многим" нужно объявить свойство типа XPCollection, имеющее только getter. Получить коллекцию ассоциированных объектов можно методом GetCollection. Чтобы XPO и XAF знали что с чем ассоциировано нужно добавить свойству атрибут Association с уникальным именем в параметре. Тот же атрибут нужно указать на другом конце ассоциации. Давайте создадим свойство Position в классе Employee.
private Position position;

[Association("Employee-Position")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.Position",
DefaultContexts.Save,
"Position cannot be empty")]
public Position Position
{
get { return position; }
set { SetPropertyValue<Position>("Position", ref position, value); }
}
С этой стороны ассоциации свойство выглядит так, как и все остальные, за исключением атрибута Association. Откомпилировав приложение, обновив базу, отредактировав модель и запустив программу мы можем создать несколько должностей и сотрудников и сассоциировать их. Примерный результат работы можно видеть на следующем изображении.

Отношение ассоциации "многие ко многим"

Не всегда достаточно отношения "один ко многим". Часто бывает необходимо, чтобы один объект хранил коллекцию других объектов, а другие объекты хранили коллекции первых объектов. XPO и XAF поддерживают и эту концепцию. Для её демонстрации, предположим, что наша организация занимается производством нескольких видов продуктов. Каждый сотрудник может принимать участие в производстве нескольких продуктов, равно как и один продукт производится несколькими сотрудниками. Создадим простой класс Product.
using System;
using DevExpress.Xpo;
using DevExpress.Persistent.Validation;

namespace DemoModule
{
public class Product :
XPObject
{
private string name;

public Product(Session session) :
base(session)
{
}

[RuleUniqueValue(
"RuleUniqueValue for DemoModule.Product.Name",
DefaultContexts.Save,
"Name must be unique")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Product.Name",
DefaultContexts.Save,
"Namecannot be empty")]
public string Name
{
get { return name; }
set { SetPropertyValue<string>("Name", ref name, value); }
}

[Association("Employee-Product")]
public XPCollection<Employee> Employees
{
get { return GetCollection<Employee>("Employees"); }
}
}
}
Для описания ассоциации "многие ко многим" используется тот же принцип, что и при использовании ассоциации "один ко многим", только на обоих концах ассоциации находятся свойства, возвращающие коллекции.
[Association("Employee-Product")]
public XPCollection<Product> Products
{
get { return GetCollection<Product>("Products"); }
}
По традиции, компилируем проект, обновляем базу данных, редактируем модель, запускаем. После некоторых манипуляций с данными мы можем получить такой результат.

Пользовательские элементы управления

XAF поддерживает создание пользовательских элементов управления. Для этого нужно реализовать контроллер для одного или нескольких отображений и в нём создать объект одного из наследников класса DevExpress.ExpressApp.Actions.ActionBase. Приведу пример лишь самого простого из них -- SimpleAction, который представляет собой кнопку на тулбаре. Наш action будет отображаться в детальном просмотре объектов класса Employee и, при активации, очищать список продуктов сотрудника.

using System;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Actions;
using DevExpress.Persistent.Base;

namespace DemoModule
{
public class EmployeeController :
ViewController<DetailView>
{
private SimpleAction cleanProductsAction;

public EmployeeController()
{
TargetObjectType = typeof(Employee);
cleanProductsAction = new SimpleAction(this,
"CleanProductsFromEmployee", PredefinedCategory.RecordEdit)
{
Caption = "Clean all products"
};
}

protected override void OnActivated()
{
base.OnActivated();
cleanProductsAction.Execute += OnCleanProductsActionExecute;
}

protected override void OnDeactivated()
{
base.OnDeactivated();
cleanProductsAction.Execute -= OnCleanProductsActionExecute;
}

private void OnCleanProductsActionExecute(object sender,
SimpleActionExecuteEventArgs e)
{
Employee employee = View.CurrentObject as Employee;
if(employee == null)
return;
int count = employee.Products.Count;
for(int i = count - 1; i >= 0; --i)
{
employee.Products.Remove(employee.Products[i]);
}
if(count > 0)
View.ObjectSpace.SetModified(employee);
}
}
}
Для создания контроллера требуется указать тип объекта, на представлении которого он будет активироваться. Первым параметром конструктора класса SimpleAction передаём ссылку на родительский контроллер. Затем передаётся идентификатор action'а и раздел, куда этот action будет помещён.

При активации контроллера вызывается метод OnActivated, в котором мы подписываемся на событие "Execute".
Контроллер содержит в себе ссылку на вид, на котором был активирован в свойстве View. Через этот объект мы можем получить текущий объект, обратившись к свойству CurrentObject. На всякий случай проверим его тип и, перебрав все продукты, удалим их. Изменения в списке XAF не заметит и кнопка "Save" не будет активирована. Для устранения этого эффекта вызываем метод SetModified из пространства объектов текущего вида.

Заключение

В этой статье я рассказал далеко не обо всех возможностях eXpress Application Framework. Чтобы узнать больше, нужно почитать официальную документацию. Возможно, в следующих статьях я расскажу что-то ещё. Скачать полный проект приведённого примера можно отсюда.