...

суббота, 18 апреля 2015 г.

[Из песочницы] Исследуем результат работы php-транслятора

Здравствуйте. Думаю, что большинство веб-программистов знает, как работает php-интерпретатор.

Для тех, кто не знает:

Вначале, написанный нами код разбирается лексическим анализатором. Далее, полученные лексемы, передаются в синтаксический анализатор. Если синтаксический анализатор дал добро, то лексемы передаются транслятору, а он, в свою очередь, генерирует так называемые opcodes (operation codes). И только после этого, в дело вступает виртуальная машина PHP (та самая Zend Engine) которая и выполняет наш алгоритм из получившихся opcodes. Opcodes так же называют эдаким php-шным ассемблером.

Данная статья расскажет вам о том, какие opcodes и в каких случаях генерируются. Конечно, рассказать про все opcodes в рамках одной статьи не получится, но в данной статье будет рассмотрен конкретный пример и на его основе мы попытаемся разобраться что к чему у этих opcodes. На мой взгляд, самое главное, что вы узнаете прочитав статью, это то, как на самом деле происходит выполнение ваших исходных текстов и, возможно, это поможет вам в лучшем понимании языка php.


Советую вам налить себе чашечку капучино или просто зеленого чая, т.к. под катом листинги opcodes и php-кода…



Постановка задачи



Не так давно, сидя на одном из php-форумов, я наткнулся на топик, в котором ТС просил помочь ему составить алгоритм, который бы из строки:


Привет {{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}}, как {твои|ваши} дела





Делал примерно следующие:


Привет Виктор, как твои дела





Как вы уже наверное поняли, необходимо было найти все вхождения {...} и достать оттуда фразы разделенные символом "|". Затем, всё вхождение {...} заменить на одну из тех фраз. Фразу нужно было выбрать случайным образом.

Здесь отчетливо видны две глобальные группы:


{{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}}





и


{твои|ваши}





По условиям изначальной задачи, уровень вложенности роли не играл, то есть все фразы, разделенные символом "|" в пределах глобальной группы, имели одинаковый вес.

Я предложил следующий вариант решения задачи:

$str = 'Привет {{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}} как {твои|ваши} дела';
$str = preg_replace_callback('#(\{[\s\S]+?\})([^\|\{\}]+)#', function($mathces)
{
$mathces[1] = str_replace(array('}','{'), '', $mathces[1]);
$arr = explode('|', $mathces[1]);
return $arr[array_rand($arr)].$mathces[2];
}, $str);
echo "$str\n";




Регулярное выражение просто выбирает все последовательности типа:


{.....} какой-то текст





В данном случае получатся 2 вхождения по 2 группы в каждом (группы выделены скобками):


( {{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}} ) ( как )





и


( {твои|ваши} ) ( дела )





Очищаем их от "{" и "}", чтобы можно было применить explode() к чистенькой строке. И в конце заменяем всю последовательность на случайную фразу из группы, полученную через explode() и rand(). Здесь, думаю, сложностей ни у кого не возникнет, т.к. алгоритм довольно простой.

Сразу скажу, что у данного варианта есть некоторые недочеты. Вот некоторые из них:


  • Если текст заканчивается символом "}", то в конце текста нужно вставить пробел

  • Если рядом стоят две пары фигурных скобок — {...}{...}, то между ними нужен хотя бы пробел




Но это скорее недостатки составленной регулярки.

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

Давайте наконец посмотри, что же нам нагенерировал транслятор:

Opcodes dump


filename: /www/patterns/www/scan/simple.php
function name: (null)
number of ops: 11
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'тут много символов'
2 1 SEND_VAL 'регулярка'
2 DECLARE_LAMBDA_FUNCTION 'имя анонимной функции'
7 3 SEND_VAL ~1
4 SEND_VAR !0
5 DO_FCALL 3 $2 'preg_replace_callback'
6 ASSIGN !0, $2
8 7 ADD_VAR ~4 !0
8 ADD_CHAR ~4 ~4, 10
9 ECHO ~4
10 > RETURN 1

Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fsimple.php0x7f68d7e7c0f:
filename: /www/patterns/www/scan/simple.php
function name: {closure}
number of ops: 22
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > RECV !0
4 1 INIT_ARRAY ~1 '%7D'
2 ADD_ARRAY_ELEMENT ~1 '%7B'
3 SEND_VAL ~1
4 SEND_VAL ''
5 FETCH_DIM_R $2 !0, 1
6 SEND_VAR $2
7 DO_FCALL 3 $3 'str_replace'
8 ASSIGN_DIM !0, 1
9 OP_DATA $3, $4
5 10 SEND_VAL '%7C'
11 FETCH_DIM_R $5 !0, 1
12 SEND_VAR $5
13 DO_FCALL 2 $6 'explode'
14 ASSIGN !1, $6
6 15 SEND_VAR !1
16 DO_FCALL 1 $8 'array_rand'
17 FETCH_DIM_R $9 !1, $8
18 FETCH_DIM_R $10 !0, 2
19 CONCAT ~11 $9, $10
20 > RETURN ~11
7 21* > RETURN null

End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fsimple.php0x7f68d7e7c0f.







«Ну вот, куча чего-то непонятного» — можете сказать вы.

Но, уверяю вас, тут нет ничего сложного, давайте разбираться.

Сразу можно увидеть, что дамп как бы разделен на 2 части:


  • 1 часть — дамп основного кода скрипта

  • 2 часть — дамп кода анонимной функции, которую мы передаем в preg_replace_callback.


Исследуем opcodes



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

Opcode, в пределах скрипта или функции, имеет несколько характеристик:


  • Номер строки в файле

  • Порядковый номер opcode в скрипте или функции

  • Имя

  • Область видимости

  • Дополнительная информация (пока что нашел ей только одно применение о котором когда-нибудь расскажу)

  • Внутренняя переменная в которую будет сохранен результат работы opcode

  • Операнды (данные) над которыми opcode производит какие-то действия




Если говорить проще, то opcode берет операнды, производит над ними какие-то действия и возвращает результат в указанную внутреннюю переменную.

Внутренние переменные — это участки памяти, которые выделяет интерпретатор при выполнения opcodes. Эти переменные имеют порядковые номера, начинающиеся с нуля и имею 3 основных типа:


  • Физические (начинается с символа "!")

  • Виртуальные (начинается с символа "$")

  • Временные (начинается с символа "~")




Давайте начнем с первого opcode (пока рассматриваем первую часть дампа) и посмотрим что получится:

line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'тут много текста'




Видно, что это первая строка скрипта и нулевой по счету opcode. Судя из названия, opcode ASSIGN присваивает одно значение другому. Вот и в данном случае мы присвоили наш тестовый текст переменной !0, а судя по дампу, !0 — это наша $str в скрипте (посмотрите на первую строку кода чтобы убедиться, что происходит присвоение текста).

Opcode в данном случае ничего не возвращает, т.к. выполняет присваивание и результат уже будет храниться в переменной !0.

Теперь давайте рассмотрим, что происходит при вызове функции. Чтобы вызвать функцию, нам нужно предать ей на вход определенные аргументы в качестве параметров. Собственно этим и занимаются opcodes под номерами 1,3 и 4

Давайте сопоставим аргументы, которые мы передали в preg_replace_callback и соответствующие opcodes:


  • Передаем регулярку: SEND_VAL 'текст регулярки'

  • Объявляем лямбда-функцию: DECLARE_LAMBDA_FUNCTION 'внутреннее имя анонимной функции'

  • Результат (функция) неявно помещается во внутреннюю временную переменную ~1

  • Передаем лямбда-функцию: SEND_VAL ~1

  • Передаем целевую строку: SEND_VAR !0




Вы наверное заметили, что в одном случае используется SEND_VAL, а в другом — SEND_VAR.

Связано это с тем, что в одних случаях, в качестве параметра, мы передаем переменную (SEND_VAR), а в других — чистое значение (SEND_VAL).

В соответствии с этим, текст регулряки и лямбду — мы передаем как значения, а исходную строку — как переменную (коей она и является).

Всё, аргументы переданы и теперь остается только вызвать функцию:

line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 DO_FCALL 3 $2 'preg_replace_callback'




Здесь, opcode с именем DO_FCALL вызывает функцию 'preg_replace_callback' и помещает результат ее работы во внутреннюю переменную $2.

Заметьте, не в физическую переменную $str (как написано в скрипте), а пока что именно во внутреннюю.

Ну и чтобы положить результат в $str (!0), используется уже знакомый нам ASSIGN:

line # * op fetch ext return operands
---------------------------------------------------------------------------------
6 ASSIGN !0, $2




Присваивает значение $2 переменной !0 (наша $str). Из этого следует, что значения возвращаемые функциями не присваиваются ждущим их переменным напрямую, а только через временную переменную.

Поехали дальше.

Последняя строка скрипта. Необходимо просто вывести получившуюся строку, но мы видим тут аж 3 opcode. Давайте посмотрим:

line # * op fetch ext return operands
---------------------------------------------------------------------------------
8 7 ADD_VAR ~4 !0
8 ADD_CHAR ~4 ~4, 10
9 ECHO ~4





  • Добавляем к временной переменной ~4 (сейчас там ничего нет) нашу переменную !0 ($str)

  • Добавляем к временной переменной ~4(в которой уже лежит значение от !0) символ переноса строки \n (код символа — 10)

  • Выводим содержимое ~4 (заметьте, что echo — это не функция, а оператор языка)




Собственно, вот и весь основной скрипт.

Стоит отметить, что всё это мероприятие завершается через opcode RETURN (хотя функции мы пока не рассматривали). Это такая особенность php — скрипт или функция обязательно должны вернуть что-нибудь в конце, даже если return явно не указан. Окончания скриптов возвращают единицу, а функции возвращают null (если не указано что-либо другое).

Теперь давайте рассмотрим анонимную функцию (вторая часть дампа).

Ее логика проста:


  • Чистим содержимое группы от "{" и "}"

  • Разделяем ее по символу "|"

  • Выбираем случайный вариант




Рассмотрим opcodes и сразу обратим внимание на compiled vars для данной функции.

Первый opcode — RECV

Он получает переданный в функцию аргумент и кладет его содержимое в !0.

Теперь нам нужно произвести ряд манипуляций для вызова функции str_replace():


  • Инициализировать массив с символами замены и предать его в качестве первого параметра

  • Передать второй параметр — пустую строку

  • Передать первую группу из вхождения в качестве третьего параметра



line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 1 INIT_ARRAY ~1 '%7D'
2 ADD_ARRAY_ELEMENT ~1 '%7B'
3 SEND_VAL ~1





  • Инициализируем массив помещая в него символ "}" (сам массив кладем в ~1)

  • Добавляем к массиву символ "{", результат помещаем туда же

  • Передаем аргумент функции



line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 SEND_VAL ''





  • Передаем в функцию пустую строку



line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 FETCH_DIM_R $2 !0, 1
6 SEND_VAR $2





  • Берем из массива !0 элемент с индексом 1 и помещаем результат в $2

  • Передаем в функцию значение $2




Из этого следует, что чтение значений из массивов происходит в 2 действия: чтение во временную переменную и уже потом использование содержимого этой переменной.

line # * op fetch ext return operands
---------------------------------------------------------------------------------
7 DO_FCALL 3 $3 'str_replace'
8 ASSIGN_DIM !0, 1
9 OP_DATA $3, $4




Тут мы сначала вызываем функцию str_replace() и результат кладем в $3.

Далее довольно интересный момент…

Внятной документации по ASSIGN_DIM нет, а по OP_DATA какой-либо документации в принципе нет, поэтому могу предположить, что ASSIGN_DIM проецирует элементы массива на ячейки памяти и возвращает указатель на ту область памяти, где находится нужный нам элемент. Указатель неявно помещается в $4. В данном случае, судя по операндам и коду, нас интересует элемент с индексом 1 в массиве !0.

Далее, OP_DATA пишет результат функции str_replace() из $3 в область памяти, указатель на которую хранится в $4.

Не понятно почему в данном случае не используется ASSIGN. Видимо это связано со спецификой хранения данных в виде массива. Хотя при модификации указателей тоже используется ASSIGN (правда там ASSIGN_REF).

Если кто-то точно знает, что за связка ASSIGN_DIM\OP_DATA и как она действительно работает, напишите пожалуйста в комментах, буду благодарен.

Далее по коду, мы вызываем explode(), для того чтобы разбить строку по символу "|":

line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 10 SEND_VAL '%7C'
11 FETCH_DIM_R $5 !0, 1
12 SEND_VAR $5
13 DO_FCALL 2 $6 'explode'
14 ASSIGN !1, $6





  • Отправляем в качестве параметра символ "|"

  • Считываем из массива !0 элемент с индексом 1 и записываем результат в $5

  • Передаем $5 в качестве параметра

  • Вызываем explode(), записывая результат в $6

  • Копируем содержимое $6 в !1


Далее, мы выбираем рандомный элемент из получившегося массива:



line # * op fetch ext return operands
---------------------------------------------------------------------------------
6 15 SEND_VAR !1
16 DO_FCALL 1 $8 'array_rand'
17 FETCH_DIM_R $9 !1, $8
18 FETCH_DIM_R $10 !0, 2
19 CONCAT ~11 $9, $10
20 > RETURN ~11





  • Отправляем !1 (массив) на вход функции array_rand(), которая, в свою очередь возвращает результат в $8

  • Затем записываем в $9 элемент массива !1 с индексом из $8

  • Записываем в $10 элемент массива !0 с индексом 2

  • Объединяем $9 и $10 в ~11

  • Возвращаем содержимое ~11




Сейчас я сознательно не расписываю всё более детально, т.к. считаю, что это уже итак понятно, ведь я всё это расписывал ранее.

Вот мы собственно и разобрали наш первый вариант алгоритма, что называется, по косточкам.

Теперь давайте двинемся дальше.
Поддержка экранированных символов в тексте



Немного подумав, я решил, что в тексте может быть любой порядок слов, не обязательно {...} текст. Да и в тексте могут встречаться такие символы как "{", "}" и "|" которые не должны быть обработаны как разделители или группы вариантов.

Исходя из этой задумки, решил, что если необходимо использовать "{", "}" и "|" в тексте, то их надо просто заэкранировать слешем, например "\{".

Да и нелепые ограничения типа необходимости пробелов в конце строки и между стоящими рядом группами, немного напрягали.

Составил себе следующую тестовую строку:


В языке {{C++|C}|{JavaScript|PHP}|C#|Java}, блоки кода можно объединять в фигурные скобки, например \{{ВАШ КОД|КАКОЙ-ТО КОД};\}\nУсловия записываются так {if(1)|if(1\|\|0)}{\{do_something();\}|\{do_some_work();\}}





Из которой должно получится что-то вроде:


В языке Java, блоки кода можно объединять в фигурные скобки, например {ВАШ КОД;}

Условия записываются так if(1||0){do_some_work();}





Для реализации такого варианта, потребовалось переработать регулярное выражение.

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

Получилось следующее:

$str = 'В языке {{C++|C}|{JavaScript|PHP}|C#|Java} блоки кода можно объединять в фигурные скобки, например \{{ВАШ КОД|КАКОЙ-ТО КОД};\}<br>Условия записываются так {if(1)|if(1\|\|0)}{\{do_something();\}|\{do_some_work();\}}';
$str = preg_replace_callback('#(?<!\\\)(\{[\s\S]+?(?<!\\\)\})(?![\|\}])#', function($mathces)
{
$mathces[1] = preg_replace('#(?<!\\\)\{|(?<!\\\)\}#', '', $mathces[1]);
$arr = preg_split('#(?<!\\\)\|#', $mathces[1]);
return $arr[array_rand($arr)];
}, $str);
$str = str_replace(array('\{', '\}', '\|'), array('{', '}', '|'), $str)."\n";
echo $str;




Регулярка работает следующим образом:


  • Находим вхождение символа "{" перед которым не стоит слеш (это показатель того, что символ не экранирован, а значит является началом группы вариантов)

  • Забираем любые символы до символа "}", перед которым нет слеша. Это значит, что даже если нам попадутся экранированные "}", они не будут считаться концом группы, т.к. по условию, мы забираем все символы до не экранированного "}"

  • (?![\|\}]) — этим утверждением мы как бы говорим, что после закрывающегося символа "}" не должно быть символов "|" и "}". Тем самым, мы хотим быть уверены, что обрабатываем группу минимального уровня вложенности (то есть глобальную группу)




Да, на этот вариант не действую ограничения прошлого варианта, но и у него есть свои тонкости. Например, данную регулярку можно легко свести с толка, вставив после любого закрывающего символа "}", символ "|".

Например:


В языке {{C++|C}|{JavaScript|PHP}|C#|Java}| блоки кода...........





Если так сделать, то алгоритм просто сожрет часть текста, посчитав его вариантом.

Но тут, как и в прошлом варианте, дело, скорее всего, в составленной регулярке.

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

Итак, в данном варианте, помимо регулярки, поменялось еще и содержимое лямбды и конец скрипта.

В конце скрипта, мы, с помощью preg_replace(), убираем все не экранированные слеши, чтобы превратить такие места как:


\{do_something();\}





в


{do_something();}





В лямбде, с помощью preg_replace(), мы чистим строку от не экранированных символов "{" и "}", чтобы остались только голые варианты, разделенные символом "|".

Затем, через preg_split(), мы получаем только те фразы, которые разделены символом "|", которому не предшествует слеш, то есть экранированный "|" не будет считаться разделителем.

Ну и возвращаем результат в виде рандомного элемента массива.

Opcodes данного примера


filename: /www/patterns/www/scan/advence.php
function name: (null)
number of ops: 21
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'много символов'
2 DECLARE_LAMBDA_FUNCTION 'внутреннее имя анонимной функции'
7 3 SEND_VAL ~1
4 SEND_VAR !0
5 DO_FCALL 3 $2 'preg_replace_callback'
6 ASSIGN !0, $2
8 7 INIT_ARRAY ~4 '%5C%7B'
8 ADD_ARRAY_ELEMENT ~4 '%5C%7D'
9 ADD_ARRAY_ELEMENT ~4 '%5C%7C'
10 SEND_VAL ~4
11 INIT_ARRAY ~5 '%7B'
12 ADD_ARRAY_ELEMENT ~5 '%7D'
13 ADD_ARRAY_ELEMENT ~5 '%7C'
14 SEND_VAL ~5
15 SEND_VAR !0
16 DO_FCALL 3 $6 'str_replace'
17 CONCAT ~7 $6, '%0A'
18 ASSIGN !0, ~7
9 19 ECHO !0
20 > RETURN 1

Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence.php0x7f0b122bb19:
filename: /www/patterns/www/scan/advence.php
function name: {closure}
number of ops: 18
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > RECV !0
4 1 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7B%7C%28%3F%3C%21%5C%5C%29%5C%7D%23'
2 SEND_VAL ''
3 FETCH_DIM_R $1 !0, 1
4 SEND_VAR $1
5 DO_FCALL 3 $2 'preg_replace'
6 ASSIGN_DIM !0, 1
7 OP_DATA $2, $3
5 8 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7C%23'
9 FETCH_DIM_R $4 !0, 1
10 SEND_VAR $4
11 DO_FCALL 2 $5 'preg_split'
12 ASSIGN !1, $5
6 13 SEND_VAR !1
14 DO_FCALL 1 $7 'array_rand'
15 FETCH_DIM_R $8 !1, $7
16 > RETURN $8
7 17* > RETURN null

End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence.php0x7f0b122bb19.







Попробуйте сами разобрать этот дамп, тут ничего нового нет.

Ну вот я и рассказал про второй вариант. Остался последний и самый интересный, как с точки зрения регулярки, так и с точки зрения рассмотрения opcodes.
Рекурсия



После того, как дискуссия на ветке форума была прекращена, в топик пришел некий товарищ и сказал, что мол «задачка уже старая и решали ее давно» и привел пример предложения, которое надо было разобрать таким образом:


{Пожалуйста|Просто} сделайте так, чтобы это предложение {изменялось {Быстро|Мгновенно} случайным образом}





И тут мне стало понятно, что нужно вводить поддержку уровня вложенности группы вариантов, а следовательно — нужна рекурсия. Причем ясно, что количество проходов равно максимальному уровню вложенности в тексте.

Начал думать. В результате чего и родился последний вариант, которым я доволен. Барабанная дробь…

while(preg_match('#(?<!\\\)\{#', $str))
{
$str = preg_replace_callback('#(?<!\\\)\{((?(?!\\\)[^\{]+?|[\s\S]+?))(?<!\\\)\}#', function($mathces)
{
$arr = preg_split('#(?<!\\\)\|#', $mathces[1]);
return $arr[array_rand($arr)];
}, $str);
}
$str = str_replace(array('\{', '\}', '\|'), array('{', '}', '|'), $str);
echo $str;




Единственное, я придумал свой вариант исходной строки, в котором было бы задействовано всё, что только можно:


{{Сегодня {утром|после обеда}}|Вчера} я {побежал|пошел|поехал{ на автобусе| на машине| на {трамвае|троллейбусе}|}} в {зоо-|компьютерный|интимный|продуктовый} магазин чтобы {купить|украсть} {костюм {совы|{человека паука|бэтмена}|Винни-Пуха|колобка}|презерватив}





После обработки, данная строка, по идее, может превратиться в нечто такое:


Сегодня утром я побежал в зоо- магазин чтобы украсть презерватив





Сначала, давайте рассмотрим регулярку и лямбду.

Находим вхождение не экранированного символа "{"

Далее, нам надо забрать любые символы до первого не экранированного символа "}", то есть, говоря другими словами, мы ищем все самые глубокие группы

Забор происходит по условию. Условие построено таким образом, чтобы механизм регулярных выражений точно определял начало и конец строки, как бы не оставляя ему другого выбора. Без такого условия, были бы проблемы со строками типа:


Привет {{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}} как {твои|ваши} дела





Т.к. в данном случае, регулярное выражение посчитало бы началом группы — "{Виктор", а на самом деле, началом первой группы является "{Антон".

В общем вся эта круговерть продолжается до тех пор, пока в тексте остались не экранированные "{". То есть после каждого прохода preg_replace_callback() — происходит подъем уровня вложенности и процесс повторяется до наступления указанного выше события.

Вы конечно можете сказать что всё это можно сделать и более простым регулярным выражением, но вспомните про поддержку "{", "}" и "|".

Да, она здесь тоже есть. И что самое главное — отсутствуют ограничения на формат текста (на сколько я мог заметить во время тестирования алгоритма).

Теперь давайте разберем то, ради чего мы все тут собрались.

Opcode dump


filename: /www/patterns/www/scan/advence2.php
function name: (null)
number of ops: 25
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'много символов'
2 1 > SEND_VAL регулярка для проверки на существование в строке не экранированных "{"
2 SEND_VAR !0
3 DO_FCALL 2 $1 'preg_match'
4 > JMPZ $1, ->12
4 5 > SEND_VAL 'длинная регулярка'
6 DECLARE_LAMBDA_FUNCTION 'внутреннее имя анонимной функции'
8 7 SEND_VAL ~2
8 SEND_VAR !0
9 DO_FCALL 3 $3 'preg_replace_callback'
10 ASSIGN !0, $3
9 11 > JMP ->1
10 12 > INIT_ARRAY ~5 '%5C%7B'
13 ADD_ARRAY_ELEMENT ~5 '%5C%7D'
14 ADD_ARRAY_ELEMENT ~5 '%5C%7C'
15 SEND_VAL ~5
16 INIT_ARRAY ~6 '%7B'
17 ADD_ARRAY_ELEMENT ~6 '%7D'
18 ADD_ARRAY_ELEMENT ~6 '%7C'
19 SEND_VAL ~6
20 SEND_VAR !0
21 DO_FCALL 3 $7 'str_replace'
22 ASSIGN !0, $7
11 23 ECHO !0
24 > RETURN 1

Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence2.php0x7f733b71526:
filename: /www/patterns/www/scan/advence2.php
function name: {closure}
number of ops: 11
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 0 > RECV !0
6 1 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7C%23'
2 FETCH_DIM_R $0 !0, 1
3 SEND_VAR $0
4 DO_FCALL 2 $1 'preg_split'
5 ASSIGN !1, $1
7 6 SEND_VAR !1
7 DO_FCALL 1 $3 'array_rand'
8 FETCH_DIM_R $4 !1, $3
9 > RETURN $4
8 10* > RETURN null

End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence2.php0x7f733b71526.







Лямбду разбирать не будем, она простая до безобразия, а вот основной код разберем, тем более что я уже слышу недоумевающие и полные интереса возгласы, которые прямо таки вопрошают рассказать им про появившиеся стрелочки (хотя мы видели их и раньше) и новый для нас вид opcodes — JMP*

Давайте по порядку. Обратим внимание на:

line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 1 > SEND_VAL 'регулярка для проверки на существование в строке не экранированных "{"'
2 SEND_VAR !0
3 DO_FCALL 2 $1 'preg_match'
4 > JMPZ $1, ->12




Это while в нашем коде. Тут мы передаем в функцию preg_match() аргументы в виде регулярки и строки, которую будем проверять на вхождения. Вызываем функцию, помещаем результат в $1.

Теперь внимательно. Opcode JPMZ — это управляющий opcode, который делает следующее:

Если операнд равен нулю, то opcode передает управление в другое место, потому JMPZ расшифровывается как «Jump If Zero».

Судя по дампу, мы можем смело сказать, что если содержимое $1 будет равно 0, то управление перейдет к opcode под номером 12.

А что там?

line # * op fetch ext return operands
---------------------------------------------------------------------------------
10 12 > INIT_ARRAY ~5 '%5C%7B'




А там 10 строка нашего скрипта, то есть мы уже находимся за пределами while.

Теперь вспоминаем как работает while.

Если условное выражение, которое мы передаем в while, вернет нам что-либо отличное от нуля, то выполнится тело while, но если оно вернет на 0 или эквивалент (пустая строка, пустой массив, false и т.д.), то нас выкинет из while. Что мы здесь собственно и наблюдаем.

Еще раз взгляните на участок opcodes отвечающих за реализацию while.

Если то, что вернула нам функция preg_match() равно 0, то выйти из цикла. preg_match() вернет нам 0, если совпадения небыли найдены и нас выкинет из while.

Далее посмотрите на еще один JMP:

line # * op fetch ext return operands
---------------------------------------------------------------------------------
9 11 > JMP ->1




Это, так называемая, безусловная передача управления, находится она, как видим, на 9 строке скрипта. А там у нас закрывающая скобка while. Что произойдет? Правильно! Очередная проверка условия перед телом цикла. Смотрим на opcode — JMP отправляет нас к opcode под номером 1. А что там? А там то, что мы уже рассматривали.

Вот собственно и вся логика. Обратите внимание, что перед именами некоторых opcodes, есть стрелки указывающие направо. Но здесь нас интересует не направление стрелок и а их расположение. Если стрелка расположена по левому краю своего столбца, то это означает вход в участок кода. Это может быть цель условного или безусловного перехода, или просто открывающаяся командная скобка.

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

Ну что, вот собственно и всё, о чем я хотел рассказать в этой статье.

Конечно, можно было бы замерит производительность всех трех вариантов (и я это делал), но, уверяю вас, что всё зависит от задачи и на разных текстах производительность будет разная.


Для дампа opcodes, использовался модуль VLD

PHP версии 5.5.14

Остальные ссылки указаны в тексте.


Надеюсь, что дал вам новые знания и вы смогли лучше понять работу php.


Спасибо за внимание и всего вам самого хорошего!


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


[Перевод] Phalcon 2 вышел



Ожидание закончилось! Phalcon 2.0 уже здесь!

После более чем года разработки, мы невероятно рады объявить о выпуске финального релиза Phalcon 2.0.



Те, кто следил за проектом внимательно, знают, что это было подвигом:



  • Создали совершено новый язык программирования — Zephir , который позволяет разработчикам писать расширения для PHP.

  • Нам пришлось переписать большую часть Phalcon 1.3.x, мы старались оставить тот же функционал, как и прежде, чтобы гарантировать обратную совместимость и упростить миграцию.

  • Zephir был значительно скорректирован после того, как мы перевели разработку на него, теперь он предлагает разработчикам больше функций и возможностей для разработчиков.

  • Есть множество дополнительных фич, которые были реализованы в версии 2.0, за что мы очень благодарны колабораторам!


Результаты, которыми мы можем гордиться:



  • Расширение Phalcon 2.0 также совместим с PHP (и даже больше) как и раньше

  • Zephir позволяет разработчикам писать собственные расширения без необходимости в знаниях C.




Установка




Данная версия может быть установлена из ветки 2.0.0, если у вас еще не установлен Zephir, следуйте следующим инструкциям:

git clone http://ift.tt/1j9abLe
git checkout 2.0.0
cd ext
sudo ./install




Стандартный метод установки также работает:

git clone http://ift.tt/1j9abLe
git checkout 2.0.0
cd build
sudo ./install




Если же Zephir уже установлен:

git clone http://ift.tt/1j9abLe
git checkout 2.0.0
zephir build




Обратите внимание, что при запуске установочного скрипта, предыдущие версии Phalcon будут удалены.

DLL-библиотека для Windows доступна на странице загрузки.


Смотрите руководство по обновлению для получения дополнительной информации об установке и обновлении до версии 2.0.


Что дальше




Наш график релизов предусматривает выпуск обновлений раз в один или два месяца, исправления безопасности, конечно же, будут происходить с максимально возможной скоростью. Цели просты: мы хотим реализовать наиболее востребованные сообществом функции :)

Минорные изменения и исправления будут выходить по мере возможности. Также мы работаем над переходом к модели LTS-релизов для нашего сообщества.


Наконец, мы собираемся начать разработку под PHP7, это переломный для всех нас момент и Zephir будет соответствовать всем требованиям разработчиков к моменту релиза 7й версии. Решение этой задачи потребует большой подготовки и у нас еще много работы впереди, но мы мотивированы как никогда и полны решимости реализовать в Zephir все те преемущества, которые предложит новый PHP.


Заключение и благодарности




Мы очень рады этому обновлению и ясно видим путь развития нашего фреймворка. Больше спасибо всем участникам! Без вас мы бы не справились. И конечно же, мы хотели бы поблагодарить всех, кто тестировал alpha/beta-версии и RC, искал баги, писал репорты и всячески помогал с обратной связью. Мы очень надеемся, что вам понравится этот релиз.

Мы хотели бы выразить нашу глубокую признательность за огромные усилия, миграции и тестирования кода в этой версии следующим людям:




Еще раз спасибо всем!

Наслаждайтесь Phalcon 2

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Что такое HL7 Continuity of Care Document (CCD)

Согласно определению в wiki, Continuity of Care Document или CCD, это стандарт, основанный на XML, и направленный на кодирования структуры и семантики медицинской карты пациента для последующего обмена.

С точки зрения разработчика мед стандартов, CCD это совместное детище комитетов HL7 International и ASTM (American Society for Testing and Materials). С семантической точки зрения, CCD есть руководство разработчика по реализации стандарта ASTM CCR (Continuity of Care Record) на основе CDA R2 (HL7 Version 3 Clinical Document Architecture Release 2). Вот такая вот запутанная история.


Проще говоря, встретились два комитета, которые долго бодались по поводу стандартов, и решили, что все те данные, которые используются в ASTM CCR (также известного как ASTM E2369-05), будут закодированы, с небольшими дополнениями, в стандарте CDA. То, что получилось, было названо Continuity of Care Document.


Стандарт описан в следующих двух документах, доступных на сайте HL7.org:



  • HL7v3 Normative Edition — HL7 Clinical Document Architecture, Release 2.0;

  • HL7 Implementation Guide: CDA Release 2 – Continuity of Care Document (CCD).






Кроме самого руководства разработчика, стандарт включает CCD и HL7 core XML схемы, CCD CSS стили и ненормативные примеры правил Schematron для валидации документов.

Прежде чем переходить дальше, небольшое отступление, как, в общем случае, разрабатываются стандарты HL7 и их артефакты. В HL7 это происходит следующим образом, на примере CCD:



  • CDA документ наследует классы от HL7 RIM (Reference Information Model) в тесном взаимодействии с типами данных HL7v3.

  • Далее, CCD документ наследует свои классы от CDA R-MIM (Refined Message Information Models).

  • Далее, на CCD классы накладываются дополнительные ограничения требуемые ASTM CCR.

  • Далее, на основе CCD R-MIM, генерируют все необходимые по стандарту HL7 артефакты, такие как XML схемы, MIF файлы и HMD (Hierarchical Message Descriptor) представления.

  • На последнем шаге CCD XML схема корректируется вручную, чтобы включить требования первоначально отсутствующие в HL7 RIM, но необходимые по стандарту ASTM CCR.


Графически это может быть представлено следующим образом (в данном примере моя собственная локализация для генерации XML схем и всех требуемых описаний отдельно для заголовка и тела CCD документа):


Далее, прочие организации или бизнес требования могут наложить дополнительные ограничения на стандарт, например, это происходит в США с их Meaningful Use (MU) Stage 1 и Stage 2.


Шаблоны


«Поскольку спецификация CDA излишне гибкая и многофункциональная» (цитата из HL7 Normative Edition в моём вольном переводе), то следующий уровень ограничений для стандарта CDA назван шаблонами или паттернами, которые определяются на уровне документов, секций и записей. Шаблоны «должны следовать детальному руководству разработчика, содержащим детальное описание как элементы документа должны быть структурированы и представлены для обеспечения клинической безопасности». Опять же, первоначальная гибкость стандарта позволяет создавать множество различных шаблонов.


Так, в настоящий момент, на уровне документов определены девять шаблонов документов. Кроме CCD туда входят Consultation Note, Diagnostic Imaging Report, Procedure Note, Discharge Summary и др. На уровне секций определено около 65 шаблонов и их количество, скорее всего, будет увеличиваться. Примерами шаблонов на уровне секций могут быть Allergies, Encounters, Medications, Problem List и т.д.


Следующая картинка схематично показывает, как группируются шаблоны документов и шаблоны секций, а также как налагаются дополнительные ограничения, например, Meaningful Use (MU).



Так, некоторые шаблоны секций могут использоваться как эксклюзивно какими-то шаблонами документов, так и входить в несколько шаблонов документов. Естественно, что на картинке показана только малая часть шаблонов документов и секций.


Как упоминалось выше, CDA не останавливается на секциях и определяет около сотни шаблонов записей. К таким шаблонам относятся, например, Age, Observation, Encounter Diagnosis, Immunization Activity и т.п. Шаблоны записей характеризуют одно из клинических состояний и подразумевают только компьютерную обработку (т.е. не предназначены для чтения представленной информации человеком). Также как и с секциями, некоторые шаблоны записей используются исключительно в одном типе документа, в то время как другие могут встречаться в нескольких типах документов. Например, шаблон записи Discharge Diet используется только в документе Discharge Summary. А шаблон запись Reason for Referral только в Referral Summary (также известном как HITSP C48). В отличие от них записи Allergies или Diagnostic Results встречаются во всех типах документов.


Вся дополнительная информация по CDA, CCD или HL7 доступна, в первую очередь, на сайте HL7.org.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Уязвимость CVE-2015-1635 (MS15-034) эксплуатируется in-the-wild


сегодня в 11:46


На этой неделе мы писали про критическое обновление MS15-034 для драйвера http.sys на Windows 7+ (включая Windows 10 TP). Злоумышленник с использованием специально сформированного заголовка запроса HTTP-протокола может удаленно исполнить код, организовать DoS-атаку или уронить систему в BSOD как на клиентских так и на серверных выпусках Windows. Сам драйвер http.sys загружается Windows на самых ранних этапах и отвечает за реализацию логики работы HTTP-протокола (реализует прикладной уровень модели OSI в Windows).


Напрямую эта уязвимость относится к серверным выпускам Windows, поскольку службы IIS, которые отвечают за механизмы реализации веб-сервера, используют этот драйвер для открытия и обслуживания HTTP-портов входящих подключений, которые и могут быть использованы для компрометации системы. Однако, на некоторых современных клиентских выпусках Windows (в зависимости от конфигурации системы и ее сервисов), работающие сервисы также могут открывать HTTP-порты для прослушивания входящих подключений.



Сама эксплуатация возможна через задание специального значения поля Range заголовка HTTP (значение регламентировано RFC 2616, 14.35.1). Ниже приведен пример такого запроса.



GET / HTTP/1.1

Host: MS15034

Range: bytes=0-18446744073709551615





Этот запрос также может быть использован для проверки системы на уязвимость.



Рис. Предложение на подпольном форуме о продаже эксплойта для MS15-034.



Update: We are seeing active exploits hitting our honeypots from 78.186.123.180. We will be going to Infocon Yellow as these scans use the DoS version, not the «detection» version of the exploit. The scans appear to be «Internet wide».





isc.sans.edu

Одна из обнаруженных версий эксплойта ITW имела вид:



GET /%7Bwelcome.png HTTP/1.1

User-Agent: Wget/1.13.4 (linux-gnu)

Accept: */*

Host: [server-ip]

Connection: Keep-Alive

Range: bytes=18-18446744073709551615





Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


[Из песочницы] Xcode: плагины для плагинов


Заинтересовавшись публикацией «Пишем свой Xcode plugin» решил написать простой тайм-трекер для Xcode. Процесс, через который я прошел — суть данной статьи. В ней мы с вами разберём несколько плагинов, которые помогут писать другие плагины быстрее и эффективнее.


Основная идея любого плагина с интерфейсом состоит в том, что он интегрируется в UI Xcode'a и выглядит максимально родным для него. Но как только мы смотрим на окно Xcode, сразу же встает вопрос: «Как понять где какой объект и как нам интегрироваться в нужный нам?» Так на нашем пути появляется первый плагин. Мы напишем простой плагин, который будет загружаться в Xcode и говорить, где какой объект расположен.



Первый плагин




Для начала, устанавливаем шаблон для плагинов и создаем плагин. Дальше все просто: для того, что бы понять, из чего состоит Xcode, необходимо вывести в лог его объекты. Для этого можно записывать логи в какой-нибудь файл или выводить их диалогами и каждый раз их закрывать. Ах, как было бы удобно выводить эту информацию прямо в консоль Xcode'a, скажете вы? Ну ничего, мы решим эту проблему нашим вторым плагином, но об этом чуть-чуть попозже. А пока, чтобы не разбираться с местоположением объектов из логов, мы будем делать скриншот окна Xcode с закрашенной областью объектов и сохранять всё это в файл.

Наш первый плагин будет состоять всего из одного метода, который будет проходить по всем 'NSView', раскрашивать их и делать скриншот текущего окна. Звучит просто, но на практике есть один небольшой нюанс: некоторые объекты 'NSView' в Xcode не могут иметь ничего кроме одной единственной 'contentView' и добавить свою на нее мы не можем. Но это не беда, мы просто будем игнорировать такие объекты и углубляться в них.


Текст метода


- (void)viewSnap:(NSView *)view {

static int i = 0;
//создаем text field с именем объекта
NSTextField *name = [[NSTextField alloc] initWithFrame:view.bounds];
name.backgroundColor = [NSColor colorWithDeviceRed:rand()%255/255.f green:rand()%255/255.f blue:rand()%255/255.f alpha:0.3];
name.textColor = [NSColor blackColor];
NSString *string = view.className?:NSStringFromClass(view.class);
name.stringValue = string?:@"unknown";
if (![view respondsToSelector:@selector(contentView)]) {//можем ли мы добавить text field?
[view addSubview:name];
//делаем снимок и сохраняем в файл
NSImage *captureImage = [[NSImage alloc] initWithData:[[NSApp keyWindow].contentView dataWithPDFInsideRect:[[NSApp keyWindow].contentView bounds]]];
[[captureImage TIFFRepresentation] writeToFile:[NSString stringWithFormat:@"%@%d.png", self.dirPath, i++] atomically:YES];

[name removeFromSuperview];
}
for (NSView *v in view.subviews) {
if ([v respondsToSelector:@selector(contentView)]) {
NSView *vv = [v performSelector:@selector(contentView) withObject:nil];
[self viewSnap:vv];
} else {
[self viewSnap:v];
}
}
}


И вызов метода:



- (void)doMenuAction {
NSWindow * window = [NSApp keyWindow];
srand(time(NULL));
[self viewSnap:window.contentView];
}







После этого можно открывать папку, куда вы сохраняли снимки и любоваться. Стоит поиграться еще с размерами текста в зависимости от размера 'NSView'.

Вот так выглядит результат:





А вот результаты в более красивом виде и после ручной обработки:


Немного картинок



Cразу же перейдём ко второму плагину. Будем выводить информацию от плагинов в Xcode консоль.

Второй плагин




От первого плагина, мы узнали, что консоль в Xcode — это 'IDEConsoleTextView' класс. Но что это вообще за класс и какие методы у него есть? Что бы узнать это, есть несколько путей:

1. Написать плагин, который найдет консоль в окне и выведет все его методы в файл

2. С помощью class-dump'a стянуть все хидеры из приватных фреймворков и пытаться найти этот класс там.

3. Идти на страничку проекта XVim и взять все приватные хидеры там.

Абсолютно неважно, каким путём пойдете вы, главное, что вы обнаружите, что консоль — сабкласс от 'NSTextView' и что она содержит в себе следующие методы: insertText:, insertNewLine :. Отлично, теперь мы можем найти консоль в окне и записать туда нужные нам строчки информации.


Теперь нам нужно добавить кнопку, отвечающую за режим логов и получить информацию от других плагинов.


После первого плагина мы знаем, что рядом с консолью есть 'DVTScopeBarView', содержащий в себе элементы управления. Туда мы и положим нашу кнопку. Смотрим в хидер 'DVTScopeBarView' и видим, что класс содержит в себе метод addViewOnRight:. Очень хорошо, значит мы можем добавить нашу кнопку на бар и не беспокоиться о положении других элементов.


Поиск IDEConsoleTextView и DVTScopeBarView


- (IDEConsoleTextView *)consoleViewInMainView:(NSView *)mainView
{
for (NSView *childView in mainView.subviews) {
if ([childView isKindOfClass:NSClassFromString(@"IDEConsoleTextView")]) {
return (IDEConsoleTextView *)childView;
} else {
NSView *v = [self consoleViewInMainView:childView];
if ([v isKindOfClass:NSClassFromString(@"IDEConsoleTextView")]) {
return (IDEConsoleTextView *)v;
}
}
}
return nil;
}

- (DVTScopeBarView *)scopeBarViewInView:(NSView *)view {
for (NSView *childView in view.subviews) {
if ([childView isKindOfClass:NSClassFromString(@"DVTScopeBarView")]) {
return (DVTScopeBarView *)childView;
} else {
NSView *v = [self scopeBarViewInView:childView];
if ([v isKindOfClass:NSClassFromString(@"DVTScopeBarView")]) {
return (DVTScopeBarView *)v;
}
}
}
return nil;
}
- (void)someMethod {
NSWindow *window = [NSApp keyWindow];
NSView *contentView = window.contentView;
IDEConsoleTextView *console = [self consoleViewInMainView:contentView];//ищем консоль
DVTScopeBarView *scopeBar = nil;
NSView *parent = console.superview;
while (!scopeBar) {
if (!parent) break;
scopeBar = [self scopeBarViewInView:parent];
parent = parent.superview;
}
//... добавляем кнопку на бар
}







Теперь мы добавили кнопку на бар и можем на окне найти консоль. Осталось как-то получать от других плагинов информацию и выводить её. Самый простой вариант: использовать 'NSNotificationCenter'. Так как плагины грузятся в среду Xcode и могу ловить от него уведомления, то можно и между плагинами отправлять и ловить их. Просто подписываемся под нужные нам уведомления и говорим консоли вывести лог. Для этого создаем функцию в клиент-файлах (файлы, которые будут использовать другие плагины), которая будет отправлять нужные нам уведомления и ловим их в нашем плагине.
Функции лога и отображение в консоли


void PluginLogWithName(NSString *pluginName, NSString *format, ...) {
NSString *name = @"";
if (pluginName.length) {
name = pluginName;
}
va_list argumentList;
va_start(argumentList, format);
NSString *string = [NSString stringWithFormat:@"%@ Plugin Console %@: ", [NSDate date], name];
NSString* msg = [[NSString alloc] initWithFormat:[NSString stringWithFormat:@"%@%@",string, format] arguments:argumentList];
NSMutableAttributedString *logString = [[NSMutableAttributedString alloc] initWithString:msg attributes:nil];
[logString setAttributes:[NSDictionary dictionaryWithObject:[NSFont fontWithName:@"Helvetica-Bold" size:15.f] forKey:NSFontAttributeName] range:NSMakeRange(0, string.length)];
[[NSNotificationCenter defaultCenter] postNotificationName:PluginLoggerShouldLogNotification object:logString];
va_end(argumentList);
}
- (void)addLog:(NSNotification *)notification {//ловим уведомление
for (NSWindow *window in [NSApp windows]) {//выводим лог во все окна
NSView *contentView = window.contentView;
IDEConsoleTextView *console = [self consoleViewInMainView:contentView];//находим консоль
console.logMode = 1;//переключаем консоль в режим редактирования
[console insertText:notification.object];//вставляем текст
[console insertNewline:@""];//переводим каретку на следующую строку
}
}




Как вы могли заметить, в лог можно выводить абсолютно любым шрифтом.


Вот и готов второй плагин. Полные исходники можно посмотреть здесь. Выглядит плагин вот так:



Третий плагин




Эх, как было бы хорошо, если бы плагины были рядом и доступ к ним был таким же, как к разделам в левой панели Xcode…

Давайте добавим свою панель в Xcode, чтобы любой желающий мог добавить свой плагин, не задумываясь об интеграции плагина с окном Xcode.

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


У нас у окна есть 'NSToolbar', куда мы и будем добавлять кнопку. Самое сложное, что тулбар не имеет методов, чтобы прямо добавить элемент. Его элементами «рулит» делегат, который переопределить мы, конечно же, не можем. Единственный метод, который имеет тулбар для добавление элементов: insertItemWithItemIdentifier:atIndex:, но сам элемент генерирует делегат. Единственный выход — посмотреть, кто же является делегатом. Может быть, есть какие-то подходы к нему? Выводим в логи класс делегата и получаем класс 'IDEToolbarDelegate'. Отлично, теперь идем в приватные хидеры, которые мы получили class-dump'ом или взяли у XVim, и ищем этот класс там. Сразу же видим интересующие нам свойства у этого класса: toolbarItemProviders и allowedItemIdentifiers. Предположительно, наш делегат содержит словарь объектов, которые как раз-таки предоставляют элементы. Выводим в логи текущее содержание toolbarItemProviders и видим примерно такой словарь:



{
"some_id":<IDEToolbarItemProxy class>,
"some_other_id":<IDEToolbarItemProxy class>,
}




Отлично, теперь у нас есть еще одна зацепка — это класс 'IDEToolbarItemProxy'. Так же смотрим его интерфейс в хидерах и видим, что он инициализируется с идентификатором (скорее всего идентификатор элемента в 'NSToolbar') и имеет свойство providerClass. Но что это за providerClass и как нам его реализовать? Чтобы понять, что должен содержать данный класс, есть два пути:

1. Вывести данные классы и их методы у всех провайдеров из словаря toolbarItemProviders ;

2. Написать пустой класс, добавить его в словарь и ловить креши от Xcode, говорящих нам, каких методов не хватает.

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


Итак, создаем класс и добавляем его к нашему делегату:


Код


IDEToolbarDelegate *delegate = (IDEToolbarDelegate *)window.toolbar.delegate;//берём делегат у тулбара
if ([delegate isKindOfClass:NSClassFromString(@"IDEToolbarDelegate")]) {
IDEToolbarItemProxy * proxy = [[NSClassFromString(@"IDEToolbarItemProxy") alloc] initWithItemIdentifier:PluginButtonIdentifier];//создаем наш прокси и нужным идентификатором
proxy.providerClass = [PluginButtonProvider class];//устанавливаем ему наш провайдер класс(пока пустой)
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:delegate.toolbarItemProviders];//берем словарь у делегата
[d setObject:proxy forKey:proxy.toolbarItemIdentifier];//добавляем наш прокси
delegate.toolbarItemProviders = d;//возвращаем словарь делегату
NSMutableArray *ar = [NSMutableArray arrayWithArray:delegate.allowedItemIdentifiers];//добавляем наш идентификатор в делегат
[ar addObject:proxy.toolbarItemIdentifier];
delegate.allowedItemIdentifiers = ar;
[window.toolbar insertItemWithItemIdentifier:PluginButtonIdentifier atIndex:window.toolbar.items.count];//вставляем наш элемент последним в тулбар
}







Устанавливаем плагин, перезапускаем Xcode и сразу же ловим креш. Смотрим логи и понимаем, что нашему классу необходим метод + (id)itemForItemIdentifier:(id)arg1 forToolbarInWindow:(id)arg2. Этот метод описан в протоколе 'IDEToolbarItemProvider'. Удаляе плагин, запускаем Xcode и добавляем данный метод. По названию метода ясно, что на вход мы получаем идентификатор и окно, а на выходе должны получить некий объект. Подобными манипуляциями, а именно методом проб и ошибок, через N-ое количество крешей и перезапусков Xcode можно выяснить, что это объект класса 'DVTViewControllerToolbarItem'. А он в свою очередь инициализируется с классом 'DVTGenericButtonViewController'. Сам объект 'DVTGenericButtonViewController' иммет вот такую инициализацию:

До 6-ой версии Xcode: initWithButton:actionBlock:itemIdentifier:window:

С 6-ой версии: initWithButton:actionBlock:setupTeardownBlock:itemIdentifier:window:

По названию метода ясно, что ему нужна кнопка, блок, который вызывается при её нажатии, идентификатор и окно.

Создаём простую кнопку и инициализируем нужные нам контроллеры:


Капелька кода


DVTGenericButtonViewController *bvc = [(DVTGenericButtonViewController*)[NSClassFromString(@"DVTGenericButtonViewController") alloc] initWithButton:button actionBlock:^(NSButton *sender){} setupTeardownBlock:nil itemIdentifier:PluginButtonIdentifier window:arg2];
DVTViewControllerToolbarItem *c = [ NSClassFromString(@"DVTViewControllerToolbarItem") toolbarItemWithViewController:bvc];







Устанавливаем плагин и перезапускаем Xcode. Теперь наша кнопка добавлена в Xcode. Осталось написать обработчик для нашей кнопки. При клике на кнопку мы хотим, что бы открывалась правая панель, если она не открыта, и добавлялся наш объект на эту панель. Открываем правую панель и запускаем наш первый плагин. Просмотрев его результаты станет ясно, что панель — это 'DVTSplitView' объект. Кроме этого, необходимо определить, как программно открыть правую панель, если она спрятана. Для этого выводим все 'NSToolbarItem' из тулбара нашего окна в лог. Мы знаем, что объект, который нам нужен является последним(если наша кнопка еще не была добавлена). Берем нужный нам 'NSToolbarItem' и смотрим кто же им управляет, то есть смотрим свойство 'target'. Таргетом нашего 'NSToolbarItem' является объект класса '_IDEWorkspacePartsVisibilityToolbarViewController'. Нам не нужно смотреть его интерфейс, так как он нам нужен лишь для того, что бы в будущем находить нужный нам 'NSToolbarItem' в окне (вдруг они будут располагаться в другой сортировке или кто-то добавить элемент до нас). Все приготовления готовы, теперь мы можем отобразить правую панель, найти её в окне и добавить наш объект на неё.
Обработка кнопки


NSWindow *window = arg2;
NSToolbarItem *item = nil;
for (NSToolbarItem *it in [[window toolbar] items]) {//ищем нужный нам тулбар айтем
if ([it.target isMemberOfClass:NSClassFromString(@"_IDEWorkspacePartsVisibilityToolbarViewController")]) {
item = it;
break;
}
}
NSSegmentedControl *control = (NSSegmentedControl *)item.view;//берем сегмент контрол из него

if ([sender state] == NSOnState) {//если кнопка включилась
if (![control isSelectedForSegment:2]) {//если правая панель спрятана
[control setSelected:YES forSegment:2];//включаем правую панель на сегменте
[item.target performSelector:item.action withObject:control];//и потравляем экшн
}
DVTSplitView *splitView = [PluginButtonProvider splitViewForWindow:window];//ищем правю панель

PanelView *myView = [[PluginPanel sharedPlugin] myViewForWindow:window];//создаем/получаем наш объект для окна
myView.frame = splitView.bounds;//устанавливаем размеры
[myView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
for (NSView *sub in splitView.subviews) {//прячем все предыдущие элементы на панели
[sub setHidden:YES];
}
[splitView addSubview:myView];//добавляем наш объект
} else {
DVTSplitView *splitView = [PluginButtonProvider splitViewForWindow:window];//ищем панель
PanelView *myView = [[PluginPanel sharedPlugin] myViewForWindow:window];//создаем/ищем наш объект
[myView removeFromSuperview];//удаляем наш объект из панели
for (NSView *sub in splitView.subviews) {//отображаем все оставшие элементы на панели
[sub setHidden:NO];
}
}







Нашим объектом будет объект 'NSView', который будет содержать 'DVTChooserView' и обычную 'NSView', в которую будет добавляться контент плагина. Почему 'DVTChooserView'? Хотелось бы, что бы панель максимально подходила к окну Xcode. Для этого запускаем первый плагин, смотрим левую панель и обнаруживаем, что 'DVTChooserView' — это как раз то, что нам нужно. 'DVTChooserView' содержит в себе 'NSMatrix' с кнопками и хороший делегат, который позволяет нам определить, когда была включена/выключена та или иная кнопка. Так же, данный объект принимает на вход объекты 'DVTChoice' и ими манипулирует. Это максимально удобно, учитывая что 'DVTChoice' содержит в себе иконку, подпись и объект, который будет обрабатывать данный объект.
Наш объект и добавление элементов


//создаем и настраиваем DVTChooserView
_chooserView = [[NSClassFromString(@"DVTChooserView") alloc] initWithFrame:NSZeroRect];
_chooserView.allowsEmptySelection = NO;
_chooserView.allowsMultipleSelection = NO;
_chooserView.delegate = self;
//метод делегата
- (void)chooserView:(DVTChooserView *)view userWillSelectChoices:(NSArray *)choices {
DVTChoice *choice = [choices lastObject];//получаем выбранный элемент
self.contentView = [[choice representedObject] view];//отображаем его контент
}
//добавляем DVTChoice в наш объект
DVTChoice *plugin = note.object;//приходит от других плагинов
if (plugin) {
NSWindow *window = [[note userInfo] objectForKey:PluginPanelWindowNotificationKey];//для какого окна добавить элемент
PanelView *panel = [self myViewForWindow:window];//берём наш объект для окна
[panel.chooserView.mutableChoices addObject:plugin];//добавляем плагин в DVTChooserView
if (!panel.contentView) {
panel.contentView = [[[[panel.chooserView mutableChoices] lastObject] representedObject] view];//если нет выбранных плагинов, отображаем его контент
}
}







Вот и всё. Мы прошли по самым интересным местам нашего третьего плагина. Все исходники лежат тут.

Добавляем плагин в нашу панель




Только что мы добавили целую панель в Xcode. Теперь давайте её чем-нибудь заполним.

Благодаря тому, что нам не нужно теперь разбираться в тонкостях Xcode, мы можем добавить наш плагин в панель всего лишь тремя строчками кода.


Три волшебных строчек


NSImage *image = [[NSImage alloc] initWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForImageResource:@"plugin_icon"]];//загружаем иконку из бандла плагина
//создаем контроллер, который будет отображать плагин. 1-ая строчка
TPViewController *c = [[TPViewController alloc] initWithNibName:@"TPView" bundle:[NSBundle bundleForClass:self.class]];
//Создаем DVTChoice для отображения иконки плагина. 2-ая строчка
DVTChoice *choice = [[NSClassFromString(@"DVTChoice") alloc] initWithTitle:@"Time" toolTip:@"Time management plugin" image:image representedObject:c];
//Отправляем уведомление нашей панели, что бы она добавила наш плагин к себе. 3-яя строчка
PluginPanelAddPlugin(choice, [[note userInfo] objectForKey:PluginPanelWindowNotificationKey]);







Теперь у нас есть своя панель в окне Xcode и мы можем добавить любой плагин на неё. Теперь часть плагинов может быть расположена в одном месте.

Напоследок — пример использования панели — простой time tracker для Xcode.


TimePlugin



This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Символьный LCD дисплей (Видео урок 1)

image

Всем привет. Решил сделать видео урок по символьному lсd-индикатору в поддержку начинающим программистам микроконтроллеров. Как говорится, лучше один раз увидеть, чем сто раз услышать.

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



В данном видеоуроке я подробно и наглядно постарался изложить основы работы с lсd-индикатором. Так как я сам всегда пишу свои программы для микроконтроллера самостоятельно и стараюсь не использовать готовых библиотек, то и поддерживаю тех, кто тоже стремится сам разбираться с внешними периферийными устройствами и создавать свои программы для микроконтроллера. Надеюсь, что данный видеоурок будет полезным для начинающих, и кто-то из них почерпнёт для себя что-то новое.



Код и PDFрус. — http://ift.tt/1DrNlt6


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


[Из песочницы] Супер муравейник на ClojureCLR

Многопоточность в Clojure выведена на новый уровень развития, поскольку там реализованы транзакции изменений памяти STM (The software transactional memory system). В качестве демонстрации Рич Хикки (божественный автор Clojure) и Дэвид Миллер (человек, который написал реализацию Clojure под .Net) предлагают программу «ants», которая моделирует муравейник. Каждый муравей там живет в отдельном потоке. Муравьи бегают по клеткам общего поля, собирают еду, носят ее в муравейник и не конфликтуют друг с другом.

Результат своих упражнений с этой программой я и хочу вынести на общее обозрение. Надеюсь, статья будет полезна тем, кто начинает знакомиться с Clojure на платформе .Net.



Сперва ссылки:


http://ift.tt/1GUqQ3S

youtu.be/xZ9AGQ3L-EI

http://ift.tt/1G1GyHq

http://ift.tt/1G1GyHs


Что сделано мною.

1. Графика. В оригинале программы муравей — это только черточка. У меня насекомые изображены натуральнее.

2. Новые герои. Кроме муравьев у меня реализованы тли и божьи коровки.

3. Взаимодействие героев. Растет трава, тля ест траву, производит сахар и откладывает яйца (размножается). Божьи коровки едят тлю и яйца. Муравьи обходят препятствия на пути. (Еще муравьи должны таскать тлю на траву и мешать божьим коровкам пожирать тлю, но это еще не реализовано.)

3. Поведение. Поведение муравьев усложнилось, они стали умнее, чем в оригинале. Добавлено поведение новых героев.

4. Графики. Видна динамика процессов на графиках. Рост травы, численность тли, количество сахара в муравейнике и в окружающем пространстве.

5. Примитивная лупа можно разглядывать детали. Правая кнопка мышки.

6. Мышка. При клике на клетку поля в консоль выводится информация содержащаяся в клетке. Колесиком мышки можно менять скорость движения насекомых.


Меняя параметры инициализации можно получать разные сценарии развития этой экосистемы. Чем быстрее растет трава, тем быстрее размножается тля и тем больше появляется сахара на поле. Трава не растет на сахаре, поэтому бурная жизнедеятельность тли сказывается на росте травы. Различные соотношения насекомых при запуске (сколько тли, сколько божьих коровок, сколько муравьев) определяют новую динамику.


Каждое насекомое видит только четыре клетки: ту на которой находится, и три клетки перед собой, по ним и ориентируется.

Муравьи реагируют на феромоновый след. Для них очень важно не терять муравейник, иначе они могут бесконечно бегать с едой «в зубах», не находя, куда ее нужно положить. Я добавил им несколько фич, связанных с феромонами, чтобы они могли эффективно ориентироваться, где еда, где муравейник.


Ближе в коду. Но начну издалека.

Для начала необходимо познакомиться с такими понятиями в Clojure, как atom, agent и ref. Их можно назвать менеджерами переменных. Код не напрямую обращается к значению переменной, а через посредника. Atom, agent и ref — это три типа посредника.



(def atom1 (atom 0))
(def agent1 (agent 0))
(def ref1 (ref 0))




Здесь мы определили atom1, agent1 и ref1. Начальное значение у всех равно 0.

(swap! atom1 inc)
(prn @atom1)
;->1

(send agent1 inc)
(prn @agent1)
;->1

(dosync
(alter ref1 inc))
(prn @ref1)
;->1




Здесь мы передаем в atom1, agent1 и ref1 функцию inc которая увеличивает значение на 1 и видим, что значения везде стали равны 1.

(reset! atom1 0)
(send agent1 (fn [_] 0))
(dosync
(ref-set ref1 0))




Здесь мы меняем текущие значения atom1, agent1 и ref1 на 0.

Пока разница между посредниками не видна, она только синтаксическая.


Определим три функции:



(defn atom1-change []
(prn "atom1-change")
(swap! atom1 inc)
(prn @atom1))

(defn agent1-change []
(prn "agent1-change")
(send agent1 inc)
(prn @agent1))

(defn ref1-change []
(prn "ref1-change")
(dosync
(alter ref1 inc))
(prn @ref1))


Вызовем их:




(atom1-change)
;"atom1-change"
;1

(prn @atom1)
;1

(agent1-change)
;"agent1-change"
;0

(prn @agent1)
;1

(ref1-change)
;"ref1-change"
;1

(prn @ref1)
;1




Мы видим, что в случае агента (send agent1 inc) вызывается асинхронно. Этим агент отличается от остальных.

Нам потребуется функция задержки. Определим ее, используя стандартный Sleep из .Net и увидим interop в действии.



(defn sleep [ms]
(. System.Threading.Thread (Sleep ms)))


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



(defn agent1-thread[x]
(sleep 1000)
(send *agent* agent1-thread)
(prn x)
(inc x))

(send agent1 agent1-thread)




*agent* это текущий агент.

Тут может показаться (мне показалось), что если поменять местами sleep и send, случится что-то страшное. Ведь send вызывается рекурсивно и асинхронно, поток должен плодиться и размножаться, однако этого не происходит и можно смело писать так:

(defn agent1-thread[x]
(send *agent* agent1-thread)
(sleep 1000)
(prn x)
(inc x))

(send agent1 agent1-thread)




Подводные грабли, однако, есть. Обратите внимание на то, что последняя сточка (inc x) возвращает значение, хранящееся в агенте, увеличенное на 1 и именно это последнее возвращаемое значение записывается в агент.

Теперь три способа положить Clojure на пол с эксепшн стэк оверфлоу:



(reset! atom1 atom1)

(dosync (ref-set ref1 ref1))

(send agent1 (fn [_] agent1))




Здесь мы записываем в качестве значений атома рефа и агента их самих. Это приводит к стэк оверфлоу.

Часто агенты используются для организации таймеров без изменения значения самих агентов, например:



(def timer (agent nil) )
(defn on-timer [_]
(sleep 1000)
(send *agent* on-timer)
(prn "tic-tac"))

(send timer on-timer)




Тут стоит заметить, что prn возвращает nil и именно этот нил записывается в значение агента.

Так как функция send возвращает сам агент, прекрасный способ выстрелить себе в ногу:

(def timer (agent nil) )
(defn on-timer [_]
(sleep 1000)
(prn "tic-tac")
(send *agent* on-timer))

(send timer on-timer)




И тут, вроде бы, даже будет все в порядке, пока не захочется посмотреть, чему равен timer, а равен он стэк оверфлоу с падением Clojure.

Следующий показательный пример:



(def agent1 (agent nil))
(def agent2 (agent nil))
(def timer (agent nil))

(def atom1 (atom 0))
(def atom2 (atom 0))

(defn atoms-change-thread1 [_]
(reset! atom1 1)
(sleep (rand-int 90))
(reset! atom2 2)
(sleep (rand-int 90))
(send *agent* atoms-change-thread1)
nil)

(defn atoms-change-thread2 [_]
(reset! atom1 3)
(sleep (rand-int 90))
(reset! atom2 4)
(sleep (rand-int 90))
(send *agent* atoms-change-thread2)
nil)

(defn on-timer [_]
(prn @atom1 @atom2)
(sleep 1000)
(send *agent* on-timer)
nil)

(send agent1 atoms-change-thread1)
(send agent2 atoms-change-thread2)
(send timer on-timer)




Здесь у нас два атома и два потока изменяющих их значения.

Первый поток пишет в атомы значения 1 и 2, второй поток — значения 3 и 4.

Также есть таймер, который раз в секунду выводит значения атомов.

Вывод будет примерно такой:

3 4

1 2

1 4

3 2

1 2

3 4

1 4

3 2


Теперь, наконец о транзакциях. Перепишем предыдущий пример, но вместо атомов у нас будут рефы.



(def agent1 (agent nil))
(def agent2 (agent nil))
(def timer (agent nil))

(def ref1 (ref 0))
(def ref2 (ref 0))

(defn refs-change-thread1 [_]
(dosync
(ref-set ref1 1)
(sleep (rand-int 90))
(ref-set ref2 2)
(sleep (rand-int 90)))
(send *agent* refs-change-thread1)
nil)

(defn refs-change-thread2 [_]
(dosync
(ref-set ref1 3)
(sleep (rand-int 90))
(ref-set ref2 4)
(sleep (rand-int 90)))
(send *agent* refs-change-thread2)
nil)

(defn on-timer [_]
(prn @ref1 @ref2)
(sleep 1000)
(send *agent* on-timer)
nil)

(send agent1 refs-change-thread1)
(send agent2 refs-change-thread2)
(send timer on-timer)




Вывод будет примерно такой

3 4

3 4

1 2

3 4

1 2

3 4

1 2

3 4

3 4

1 2

Теперь становится понятно, зачем писать dosync. Это определение границ транзакции. Изменения рефов происходит транзакционно. Этим они и отличаются от остальных.


Хотя, если заменить рефы на агенты мы тоже увидим транзакционность. Агенты также действуют в STM и границами транзакции является вся функция изменяющая значение агента.



(def agent1 (agent nil))
(def agent2 (agent nil))
(def timer (agent nil))

(def agent3 (agent 0))
(def agent4 (agent 0))

(defn agents-change-thread1 [_]
(send agent3 (fn [_] 1))
(sleep (rand-int 90))
(send agent4 (fn [_] 2))
(sleep (rand-int 90))
(send *agent* agents-change-thread1)
nil)

(defn agents-change-thread2 [_]
(send agent3 (fn [_] 3))
(sleep (rand-int 90))
(send agent4 (fn [_] 4))
(sleep (rand-int 90))
(send *agent* agents-change-thread2)
nil)

(defn on-timer [_]
(prn @agent3 @agent4)
(sleep 1000)
(send *agent* on-timer)
nil)

(send agent1 agents-change-thread1)
(send agent2 agents-change-thread2)
(send timer on-timer)




1 2

3 4

1 2

1 2

3 4

1 2

1 2

1 2

Наконец, после всего этого можно вернуться к муравейнику. Если сильно упростить задачу, то выглядеть она будет так:



(def v [1 1 0 0 0])
(def world (vec
(map
(fn [x] (ref x))
v)))
(defn place [x] (world x))
(def agent1 (agent 0))
(def agent2 (agent 1))
(def agent-show-world (agent nil))

(defn agent-change [x]
(let [old (place x)
new-coord (rand-int (count world))
new (place new-coord)]
(sleep (rand-int 50))
(if (= @old 1)
(do
(send *agent* agent-change)
(dosync
(if (= @new 0)
(do
(ref-set old 0)
(ref-set new 1)
new-coord)
x)))
(prn "agent " *agent* "is out"))))

(defn show-world [_]
(sleep 1000)
(send *agent* show-world)
(prn (map (fn [x] (deref x)) world)))

(send agent-show-world show-world)
(send agent1 agent-change)
(send agent2 agent-change)




Примерный вывод будет такой

(1 0 0 1 0)

(0 0 1 1 0)

(1 0 0 1 0)

(0 1 1 0 0)

(0 0 1 1 0)

(1 0 0 1 0)

(0 0 1 1 0)

(0 0 1 1 0)

(1 0 1 0 0)

(1 0 0 1 0)

(0 0 1 0 1)

(0 1 0 1 0)

(1 0 1 0 0)

(1 0 0 1 0)

(0 0 0 1 1)

(0 1 1 0 0)

(1 1 0 0 0)

(1 0 1 0 0)

(0 0 0 1 1)

(1 0 1 0 0)

(1 0 0 0 1)

(1 0 0 1 0)

(0 1 0 0 1)

Здесь мы определили вектор v в нем два первых элемента единицы, а остальные — нули. В контексте муравейника будем считать, что единицы — это муравьи, а нули — свободные клетки. Сделали вектор world — это вектор рефов, значениями которых являются элементы вектора v. У нас есть два агента значениями которых являются координаты вектора world. И есть два потока, перемещающих единицы по вектору world. Благодаря транзакционности два потока не запишут одновременно единицу в одну и ту же клетку.


Обратите внимание что в транзакцию обернута не только запись, но и чтение-проверка:



(dosync
(if (= @new 0)
(do
(ref-set old 0)
(ref-set new 1)
new-coord)
x)))




Если по новой, случайно сгенерируемой, координате находится 0, т.е. пустая ячейка, тогда пишем в старую ячейку 0, а в новую 1. Таким образом перемещаем муравья из старой координаты в новую.

Если обернуть в транзакцию только запись, будет не потокобезопасно.



(defn agent-change [x]
(let [old (place x)
new-coord (rand-int (count v))
new (place new-coord)]
(sleep (rand-int 50))
(if (= @old 1)
(do
(send *agent* agent-change)
(if (= @new 0)
(do
(dosync
(ref-set old 0)
(ref-set new 1))
new-coord)
x))
(prn "agent " *agent* "is out"))))


С таким вариантом функции вывод будет примерно такой:

(0 0 0 1 1)

(1 0 1 0 0)

(0 0 1 0 1)

(1 0 0 0 1)

«agent » #<Agent@549043: 4> «is out»

(1 0 0 0 0)

(0 0 1 0 0)

(0 0 1 0 0)

(0 1 0 0 0)

(0 0 0 1 0)

(0 0 1 0 0)


Т.е. потоки записали одновременно единицу в одно и тоже место, потом один поток угнал эту единицу в новое место, а второй не обнаружил своей единицы на том месте где оставил и свернулся. Все. Был муравей и не стало. Вернее два муравья слились воедино.


Что касается многопоточности, пожалуй, это все.


Как реализована графика:



(def ant-vert-bitmap
'(0 0 0 0 0 0 0 0 0 2 2 0 0 0 1 1 0 0 0 0
0 0 0 0 0 1 1 0 2 2 2 2 0 1 0 0 0 0 0 0
0 0 0 0 1 0 0 1 2 3 3 2 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 2 2 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 1 5 5 1 0 0 0 0 0 0 0 0
0 0 0 0 1 0 0 0 1 4 4 1 0 0 0 1 0 0 0 0
0 0 0 0 0 1 0 0 0 1 1 0 0 0 1 0 1 0 0 0
0 0 0 0 0 0 1 0 1 5 5 1 0 1 0 0 0 1 0 0
0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 1 4 4 1 0 0 0 0 0 0 0 0
0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0
0 0 0 0 0 0 0 1 5 5 5 5 1 0 0 0 1 0 0 0
0 0 0 0 0 0 0 1 5 5 5 5 1 1 1 0 0 1 0 0
0 0 0 0 0 1 1 1 5 4 4 5 1 0 0 1 0 0 0 0
0 0 0 0 1 0 0 0 1 1 1 1 0 0 0 0 1 0 0 0
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))

(def ant-diag-bitmap
'(0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 2 2 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 3 2 1 0
0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 2 2 1 0 1
0 0 0 0 0 0 0 0 1 0 0 0 1 5 5 1 1 0 0 0
0 0 0 0 0 0 0 0 1 0 0 0 1 4 5 1 0 0 0 0
0 0 0 0 1 1 1 0 1 0 1 1 1 1 1 0 0 0 0 0
0 0 0 0 0 0 1 0 1 1 1 5 1 0 0 0 0 0 0 0
0 0 1 0 0 0 1 1 1 5 4 1 1 0 0 0 0 0 0 0
0 1 0 1 0 1 5 5 5 1 5 1 0 1 1 0 0 0 0 0
0 0 0 0 1 5 5 5 5 5 1 0 0 0 0 1 0 0 0 0
0 0 0 0 1 5 4 5 5 5 1 1 1 1 0 0 0 0 0 0
0 0 0 0 1 5 4 4 5 5 1 0 0 1 0 0 0 0 0 0
0 0 0 0 1 5 5 5 5 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))




Это муравей. Есть функции поворота и флипа этих матриц. Дальше все это рендерится в .Net Bitmap:

(defn render-bitmap [bitmap bit color]
(let [bit-pos (positions #{bit} bitmap)
rendered-bitmap (Bitmap. bitmaps-dim bitmaps-dim)]
(doseq [b bit-pos]
(let [dy (quot b bitmaps-dim)
dx (rem b bitmaps-dim)]
(.SetPixel rendered-bitmap dx dy color)))
rendered-bitmap))


Все картинки кэшируются. При отрисовке кадра все берется из кэша.


Ну и что касается Windows Form и Chart то вот код:



(def current-wins (atom nil))
(def win-app (agent nil))
(def winforms-app-inited? (atom false))

(def chart (atom nil))
(def series (atom nil))

(defn get-series [series-name]
(first (filter
(fn [x]
(if (= (. x Name) series-name) true false)) @series)))

(defn add-xy [series-name x y]
(let [series (get-series series-name)]
(when series
(.AddXY (.Points series) x y)
(when (> (.Count (.Points series)) 500) (.RemoveAt (.Points series) 0)))))

(defn create-series [chart]
(let
[series1 (. chart Series)]
(.Add series1 "herb")
(.Add series1 "sugar")
(.Add series1 "anthill-sugar")
(.Add series1 "aphises")

(doseq [s series1]
(doto s
(.set_ChartType SeriesChartType/Spline)
(.set_IsVisibleInLegend true)
))
(reset! series series1)
(doto (get-series "herb")
(.set_Color Color/Green)
(.set_LegendText "Herb")
)
(doto (get-series "sugar")
(.set_Color Color/White)
(.set_LegendText "Free sugar")
)
(doto (get-series "anthill-sugar")
(.set_Color (Color/FromArgb 255 115 61 0))
(.set_LegendText "Anthill sugar")
)
(doto (get-series "aphises")
(.set_Color (ControlPaint/Light Color/Green))
(.set_LegendText "Aphises")
)))

(defn chart-update[chart]
(add-xy "anthill-sugar" @world-time (anthill-sugar-calc))
(add-xy "herb" @world-time (herb-calc))
(add-xy "sugar" @world-time (free-sugar-calc))
(add-xy "aphises" @world-time (count @aphises))

(let [chart-areas (. chart ChartAreas)
chart-area (first chart-areas)
axis-x (. chart-area AxisX)]
(doto axis-x
(.set_Minimum (if (> @world-time 500) (- @world-time 500) 0))
(.set_Maximum (if (> @world-time 500) @world-time 500)))))

(defn create-form []
(let [form (Form.)
panel (Panel.)
animation-timer (Timer.)
world-timer (Timer.)
chart1 (Chart.)
series1 (. chart1 Series)]
(doto chart1
(.set_Name "chart1")
(.set_Location (new Point size 0))
(.set_Size (Size. size size))
(.set_BackColor (ControlPaint/Light bgcolor)))

(.Add (. chart1 ChartAreas) "MainChartArea")
(.Add (. chart1 Legends) "Legend")

(doto (first (. chart1 ChartAreas))
(.set_BackColor bgcolor))

(doto (first (. chart1 Legends))
(.set_BackColor bgcolor))

(create-series chart1)
(reset! chart chart1)
(chart-update chart1)

(let [chart-areas (. chart1 ChartAreas)
chart-area (first chart-areas)
axis-x (. chart-area AxisX)
axis-y (. chart-area AxisY)]
(doto axis-x (.set_IsStartedFromZero true))
(doto axis-y (.set_IsStartedFromZero true)))

(doto panel
(.set_Location (new Point 0 0))
(.set_Name "panel1")
(.set_Size (Size. size size))
(.add_Click
(gen-delegate EventHandler [sender args]
(when (= (.Button args) MouseButtons/Right)
(swap! show-lens? (fn [x] (not x))))
(when (= (.Button args) MouseButtons/Left)
(let [mouse-x (@mouse-pos 0)
mouse-y (@mouse-pos 1)
x (/ mouse-x scale)
y (/ mouse-y scale)
p (place [x y])]
(prn [x y] @p)
(.Focus panel)))))
(.add_MouseMove
(gen-delegate MouseEventHandler [sender args]
(reset! mouse-pos [
(* (quot (.X args) scale) scale)
(* (quot (.Y args) scale) scale)])))
(.add_MouseWheel
(gen-delegate MouseEventHandler [sender args]
(let [f (fn [x]
(let
[new-sleep (+ x (* 50 (/ (.Delta args) 120)))]
(if (> new-sleep 0) new-sleep 0)))]
(swap! ant-sleep-ms f)
(swap! ladybug-sleep-ms f)
(swap! aphis-sleep-ms f)
(prn @ant-sleep-ms)))))

(doto animation-timer
(.set_Interval animation-sleep-ms)
(.set_Enabled true)
(.add_Tick (gen-delegate EventHandler [sender args]
(do
(when @buf-graph
(.Render (@buf-graph 0) (@buf-graph 1)))
(reset! rectangles-in-cells [])
(reset! rendered-bitmaps [])
(let [v (vec (for [x (range dim) y (range dim)]
@(place [x y])))]
(dorun
(for [x (range dim) y (range dim)]
(render-place (v (+ (* x dim) y)) x y)))
(reset! buf-graph (render panel))
(when @show-lens?
(reset! buf-graph (render-lens))))))))

(doto world-timer
(.set_Interval 5000)
(.set_Enabled true)
(.add_Tick (gen-delegate EventHandler [sender args]
(swap! world-time inc)
(chart-update chart1))))

(doto (.Controls form)
(.Add panel)
(.Add chart1))
(doto form
(.set_ClientSize (Size. (* size 2) size))
(.set_Text "Super Ants"))
form))

(defn init-winforms-app []
(when-not @winforms-app-inited?
(Application/EnableVisualStyles)
(Application/SetCompatibleTextRenderingDefault false)
(reset! winforms-app-inited? true)))

(defn start-gui [x]
(init-winforms-app)
(reset! current-wins (create-form))
(Application/Run @current-wins))


Надеюсь, было не скучно.


Спасибо за внимание.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.