Русский
Русский
English
Статистика
Реклама

C

РАЗБИРАЕМ АТАКИ НА KERBEROS С ПОМОЩЬЮ RUBEUS. ЧАСТЬ 1

19.06.2020 14:16:52 | Автор: admin


Rubeus это инструмент, совместимый с С# версии 3.0 (.NET 3.5), предназначенный для проведения атак на компоненты Kerberos на уровне трафика и хоста. Может успешно работать как с внешней машины (хостовой), так и внутри доменной сети (клиентского доменного хоста).

С помощью данного инструмента можно реализовать следующие атаки:

  1. Kerberos User Enumeration and Brute Force
  2. Kerberoast
  3. AS-REP Roasting
  4. Silver Ticket
  5. Golden Ticket
  6. Overpass The Hash/Pass The Key (PTK)
  7. Pass-The-Ticket / Ticket Dump
  8. Unconstrained Delegation
  9. Constrained Delegation

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

А пока немного терминологии:

Kerberos сетевой протокол аутентификации, который предлагает механизм взаимной аутентификации клиента и сервера перед установлением связи между ними.
Клиентский компьютер компьютер, которому требуется доступ к службе, поддерживающей аутентификацию Kerberos.
Сервисный компьютер сервер/компьютер, на котором размещается Сервис, поддерживающий аутентификацию Kerberos KDC (Центр распространения ключей).
SPN (имя участника службы) это имя службы, связанной с учетной записью пользователя, в которой будет работать служба. Эта привязка выполняется в LDAP путем установки значения для атрибута servicePrincipalName. SPN имеет формат вида SERVICE/hostname
Ticket (билет) зашифрованный пакет данных, который выдается доверенным центром аутентификации KDC.

Когда пользователь выполняет первичную аутентификацию, после успешного подтверждения его подлинности, KDC выдает первичное удостоверение пользователя для доступа к сетевым ресурсам Ticket Granting Ticket (TGT).



В дальнейшем, при обращении к отдельным ресурсам сети пользователь, предъявляя TGT, получает от KDC удостоверение для доступа к конкретному сетевому ресурсу Service Ticket (TGS).



Итак, смоделируем ситуацию, когда нам удалось получить доступ к домену AD, но по каким-либо причинам нет возможности для тестирования использовать собственную хостовую ОС (Kali, BackTrack и т.п.), поэтому тестировать наш домен MEOW.LOCAL мы будем изнутри с доменной клиентской машины Windows 10. Использовать будем уже скомпилированную версию Rubeus из состава Ghostpack-CompiledBinaries.

С помощью VMware создадим виртуальные машины на операционных системах Windows Server 2016 (контроллер домена), Windows 10 (доменная машина), развернем домен meow.local и присвоим статичные IP-адреса машинам:

контроллер домена DC-16 (192.168.0.201);
доменная машина Barscomp (192.168.0.202).

В Active Directory создадим доменных пользователей ADadmin, Barsik, User1, User2.

В принципе, на этом пока всё, давайте проверим.

KERBEROS BRUTE-FORCE


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

Следующая команда выполнит перебор пользователей по словарю users.txt и выполнит брутфорс паролей по словарю pass.txt



В результате мы получили существующих пользователей и пароли, так же Rubeus любезно получил тикеты (TGT) и сохранил их на нашем компьютере. Перейдём к более продвинутым техникам атак на протокол Kerberos.

KERBEROASTING


Для проведения данной атаки необходимо создать учётную запись у нас это будет iis_svc, отвечающая за взаимодействие с IIS-сервисом на сервере DC-16.meow.local (наш КД). Ей нужно присвоить ей атрибут SPN.

  • Cоздаём сервисную учетную запись IIS
    New-ADUser -Name "IIS Service Account `
    -SamAccountName iis_svc -UserPrincipalName iis_svc@meow.local `
    -ServicePrincipalNames "HTTP/dc-16.meow.local `
    -AccountPassword (convertto-securestring "S3rv1ce!" -asplaintext -force) `
    -PasswordNeverExpires $True `
    -PassThru | Enable-ADAccount

  • Hастроим конфигурацию IIS сервера
    Import-Module WebAdministration
  • Удаляем дефолтный вебсайт
    Remove-Item 'IIS:\Sites\Default Web Site' -Confirm:$false -Recurse
  • Cоздаём новый пул приложений с использованием учётной записи IIS
    $appPool = New-WebAppPool -Name dc-16.meow.local
    $appPool.processModel.identityType = 3
    $appPool.processModel.userName = MEOW\iis_svc
    $appPool.processModel.password = S3rv1ce!
    $appPool | Set-Item

  • Создаём новый вебсайт и включаем проверку подлинности Windows
    $WebSite = New-Website -Name dc-16.meow.local -PhysicalPath C:\InetPub\WWWRoot -ApplicationPool ($appPool.Name) -HostHeader dc-16.meow.local

    Set-WebConfigurationProperty -Filter /system.WebServer/security/authentication/anonymousAuthentication `
    -Name enabled -Value $false -Location $Fqdn

    Set-WebConfigurationProperty -Filter /system.WebServer/security/authentication/windowsAuthentication `
    -Name enabled -Value $true -Location $Fqdn

    Set-WebConfigurationProperty -Filter /system.webServer/security/authentication/windowsAuthentication `
    -Name useAppPoolCredentials -Value $true -Location $Fqdn


Следующая команда выведет список всех SPNов в домене


Как видно из скриншота, пользователь iis_svc (IIS Service Account) имеет SPN HTTP/dc-16.meow.local.

Продолжаем наш эксперимент. Что же происходит, когда пользователь со своей доменной машины обращается к вебсайту IIS сервиса:
на доменной машине Barscomp запустим захват пакетов в Wireshark и через любой бразуер обратимся по адресу dc-16.meow.local;
так как мы включили проверку подлинности Windows, то появится такое окно, где требуется ввести данные доменной учётной записи пользователя:



проходим аутентификацию, попадаем на вебсайт, но это нас не интересует, посмотрим, что за это время успел поймать Wireshark:



первые два пакета AS-REQ и AS-REP являются способом, которым пользователь аутентифицируется с помощью KDC и извлекает TGT. Наибольший интерес представляют пакеты TGS-REQ и TGS-REP.

Рассмотрим содержимое пакета TGS-REQ:



Здесь мы видим, что посылается запрос для имени участника службы (SPN) HTTP/dc-16.meow.local, связанного с учетной записью iis_svc.

В пакете TGS-REP возвращается билет TGS, который зашифрован с использованием пароля учетной записи meow.local\iis_svc. Соответственно, расшифровав данный билет, мы получим пароль сервисной учетной записи. Попробуем на практике.



Давайте воспользуемся инструментом, которому посвящена данная статья (если кто забыл это Rubeus).

Итак, для проведения этой атаки используется экшн kerberoast, добавив флаг /stats, мы можем выполнить проверку на наличие пользователей с установленным SPN, уязвимых к атаке данного типа.



Теперь произведем атаку на учётную запись, использующую RC4 шифрование, а затем установим для неё AES-128/AES-256 шифрование и попробуем ещё раз.

Итак, под учетной записью meow.local/Barsik (шифрование RC4) запускаем рубеус командой Rubeus.exe kerberoast:



В результате мы получили хэш TGS (зашифрованный по алгоритму RC4) для учетной записи iis_svc.

Включим AES шифрование:



Повторим:



Мы также получили TGS, только зашифрованный по алгоритму AES-128/AES-256

Оба эти хэша ломаются одинаково хорошо утилитами hashcat и JohnTheRipper







Обратите внимание на тип хэша в данной атаке Kerberos 5 TGS-REP etype 23

ASREPRoast


Для начала немного поговорим о предварительной аутентификации Kerberos. При обычных операциях в среде Windows Kerberos клиент отправляет в KDC запрос (пакет AS-REQ), содержащий временную метку (timestamp), зашифрованную хэшем пароля пользователя. Эта структура в пакете называется PA-ENC-TIMESTAMP и встроена в PA-DATA (данные предварительной авторизации), более подробно описано на странице 60 RFC4120 и используется в Kerberos версии 5. Затем KDC расшифровывает временную метку, что бы проверить валидность пользователя, отправившего AS-REQ, а затем возвращает AS-REP и продолжает обычные процедуры аутентификации.

В AS-REP сам билет зашифрован сервисным ключом (в данном случае хэшем krbtgt), зашифрованная часть подписывается паролем пользователя, для которого мы отправляем AS-REQ.

В современных средах Windows все учетные записи пользователей требуют предварительной проверки подлинности Kerberos, но, что интересно, по умолчанию, Windows сначала пытается выполнить обмен AS-REQ/AS-REP без предварительной проверки подлинности (не отправляя зашифрованную метку времени):



Это происходит из-за того, что клиент заранее не знает поддерживаемые типы шифрования (etypes), подробно описано в разделе 2.2 RFC6113.

Данная атака возможна, если учетной записи пользователя разрешена аутентификация без предварительной проверки подлинности Kerberos. Мы можем отправить AS-REQ для пользователя, у которого отключена предварительная проверка подлинности, и сразу получить AS-REP хэш, расшифровав который получим пароль пользователя.

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



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





Есть! Мы получили AS-REP хэш, сломаем его через hashcat и JTR.







Обратите внимание, здесь я указываю хэшкату тип хэша 18200 (в Kerberoast был 13100), который означает что ломать надо хэш Kerberos 5 AS-REP etype 23.

SILVER TICKET


Суть данной атаки в том, чтобы подделать TGS для скомпрометированной службы и получить максимальные права на этом сервисе, при этом KDC здесь не принимает никакого участия. Для реализации данной атаки необходимо знать NTLM-хэш пароля учетной службы (в нашем примере meow.local\iis_svc из примера с Kerberoasting). Атакующий может создать блок данных, соответствующий TGT-REP, вот упрощенный пример поддельного билета:

realm: meow.local
sname: http\dc-16.meow.local
enc-part: Зашифровано скомпрометированным NTLM-хэшем
key: 0x379DC4FA152BA1C Произвольный ключ сеанса
crealm: meow.local
cname: BadCat
authtime: 2050/01/01 00:00:00 Срок действия билета
authorization-data: поддельный PAC, с необходимыми правами доступа к сервису

Стоит учитывать, что PAC имеет двойную подпись: первая подпись использует секрет учетной записи службы, а вторая использует секрет контроллера домена (секрет учетной записи krbtgt). Атакующий знает только секрет учетной записи службы, поэтому вторую подпись он подделать не может. Однако когда сервис получает этот билет, он обычно проверяет только первую подпись. Это связано с тем, что некоторым учетным записям служб предоставлена привилегия действовать как часть операционной системы (подробнее тут ). Для таких служб Silver Ticket будет работать, даже если пароль krbtgt будет изменен, но пока не изменится пароль учетной записи самой службы.

Непосредственно с помощью Rubeus создать Silver Ticket не получится, но это можно сделать с помощью Mimikatz.



SID идентификатор пользователя (получен командой whoami /user);
Domain наш домен meow.local;
ID желаемый идентификатор безопасности (1001 в нашем случае соответствует для учетной записи Adadmin, имеющей права администратора);
Target атакуемый хост с запущенным скомпрометированным сервисом;
Service атакуемый сервис (у нас HTTP);
RC4 NTLM-хэш пароля учётной записи meow.local\iis_svc (учетная запись службы);
User имя пользователя (может быть любым).
Mimikatz сохранит поддельный TGS в файл. Далее, командой klist.exe проверим наличие действующих билетов, с помощью Rubeus подгрузим поддельный билет (в примере файл silver.kirbi) в текущую сессию пользователя, ещё раз проверим klist и наконец отправим веб-запрос на dc-16.meow.local.



Авторизация прошла успешно и мы получили двухсотый ответ, заглянем в логи на dc-16.



Видим, что был произведен вход в систему под учетной записью BadCat (в домене такой учетки нет) с привилегиями доменного администратора Adadmin, о чем свидетельствует ИД безопасности, а точнее последние 4 цифры 1001.

GOLDEN TICKET


Эта атака напоминает серебряный билет, только атакующий создает TGT с использованием NTLM-хэша учетной записи krbtgt. Преимущества золотого билета в том, что он даёт возможность получения доступа к любому сервису или любому хосту внутри домена.

NTLM-хэш учетной записи krbtgt можно получить из процесса lsass, файла NTDS.dit или через атаку DCSync, но потребуются административные права.

С помощью Mimikatz создадим Golden ticket:



Здесь я использовал идентификатор безопасности (id) 500, чтобы получить права администратора системы (можно указать любой другой), NTLM-хэш (rc4) учетной записи krbtgt и пользователя VeryBadCat.

Проверим список действующих тикетов в сессии, добавим золотой, проверим и подключимся к КД, используя PsExec.







С таким билетом открыты любые доменные двери =)

На этом заканчиваю первую часть статьи, посвященную инструменту Rubeus. В целом, инструмент с большинством задач справляется и определенно найдет своих поклонников, но лично мне всё-таки удобнее использовать утилиты из состава Impacket, хотя пополнить свой арсенал и уметь им грамотно работать никогда лишним не будет =).

А в следующей части разберем ещё несколько техник захвата и продвижения по AD с помощью Rubeus.

Всем добра, не болейте)
Подробнее..

Маленькое удобство, способное прекратить вселенские споры

22.06.2020 00:17:16 | Автор: admin
Все те, кто пишет на Си-подобных языках, знакомы с двумя немного отличающимися стилями кода. Выглядят они вот так:

for (int i=0;i<10;i++) {    printf("Hello world!");    printf("Hello world again!");}


for (int i=0;i<10;i++){    printf("Hello world!");    printf("Hello world again!");}


Различий немного, но споры вокруг них не затихают из-за неготовых отказаться от привычного стиля разработчиков. Поэтому нам нужен компромисс. Вот его-то и предлагается обсудить далее.

Компромиссное решение могло бы выглядеть вот так:

image

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

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

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

А теперь подумаем, как указанные пожелания уживаются в предлагаемом подходе. Строчки у нас теперь экономятся не только в верхней части блока кода, но и в нижней! Оба лагеря должны оценить такой огромный плюс.

И с другой стороны красота остаётся нетронутой! Визуально блок кода выделяется самой симметричной из всех симметричных конструкцией. Глаза всё так же легко найдут начало и конец. Ну и возражать против такого стиля будет просто некому, ведь оппоненты наверняка все перейдут в лагерь сторонников подхода.

Таким образом нам остаётся лишь рассмотреть алгоритм замены фигурных скобок на знаки подчёркивания.

Алгоритм замены скобок.



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

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

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

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

Но остаётся небольшая сложность в виде бесшовного совмещения процесса редактирования с отображением замен в виде подчёркиваний.

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

Теперь изменения помельче. Если что-то правится внутри блока, то опять делать ничего не нужно. Но если к блоку добавляется новая строка, то самым логичным было бы ожидать, что разработчик установит курсор в конец последней строки и нажмёт enter. После этого мы ждём ввода первого символа и удаляем нижнее подчёркивание из начала последней строки и вставляем его в начало следующей (пока ещё пустой). Далее разработчик пишет текст после подчёркивания и некоторого набора пробелов (или табуляции), обозначающего отступ блока.

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

Выведем общие правила, соблюдение которых позволит достаточно просто производить замены.

Пока мы в блоке текста, включая крайние строки, делать ничего не нужно. Когда мы нажимаем enter в любом месте последней строки кроме конца нужно перенести подчёркивание. Если enter нажимается в конце строки ждём ввода символа. Когда мы нажимаем backspace в последней строке в позиции подчёркивания нужно переместить текущее подчёркивание на одну линию выше. И разумеется, при всех операциях нужно не забывать вставлять и удалять скобки в тот текст, который будет сохраняться на диск.

Вот и весь алгоритм. Остаётся дождаться, когда знатоки плагинов сделают нам всем такой приятный подарок. Ну а за ними подтянется и спецификация языка, ведь как ей не соответствовать тому, что удобно, да ещё и используется очень многими?

И напоследок о символе overscore, который я здесь условно назвал верхним подчёркиванием. Его, к сожалению, нет на стандартной клавиатуре, а потому нет возможности вводить его напрямую без помощи редактора. Именно поэтому важна помощь IDE. Хотя теоретически можно вводить последовательность юникода (\u00af = ), которую некоторые редакторы автоматически преобразуют в символ overscore, но всё же делать так каждый раз, когда нам нужна скобка, было бы просто издевательством над разработчиками. Поэтому и нужны плагины, ну и изменения в спецификациях языков.

Всё, ждём срочных обновлений спецификаций языков и массу удобных плагинов для всех возможных IDE :)
Подробнее..
Категории: Javascript , C++ , C , Java , Код , Си , Стиль кода

Из песочницы Как С-разработчик у JavaScript плохому учился

27.06.2020 16:12:41 | Автор: admin


Недавно мне на глаза попалась одна статья на Хабре. В ней сравниваются C# и JavaScript. На мой взгляд, сравнивать их всё равно что сравнивать луну и солнце, которые, если верить классику, не враждуют на небе. Эта статья напомнила мне о другой публикации. В ней речь идёт о сценариях неожиданного и неочевидного поведения JavaScript, а C# не упоминается от слова совсем, но живое любопытство сподвигло меня попытаться повторить подобное поведение на другом языке.

Внимание


Материал носит исследовательско-развлекательный характер, а описанные приёмы, как должно быть ясно из заголовка, не рекомендованы к использованию в продуктовой разработке.

Примеры написаны под .NET Core 3.1.

Что в сухом остатке?


В JavaScript возможно применять оператор получения остатка от деления к числами с плавающей точкой. Работает это так:

3.14 % 5 // 3.1413.14 % 5 // 3.1400000000000006

При этом числа имеют тип Number и хранятся в 64 битах в соответствии со стандартом IEEE 754. В .NET у этого типа есть брат-близнец System.Double или просто double. Если числовой литерал содержит плавающую точку, то он по умолчанию приводится к double. Намерение можно выразить явно, добавив к числу суффикс d или D. Возможность делить double с остатком тоже имеется. Так что пробуем. И Бинго!

Console.WriteLine(3.14d % 5); // 3,14Console.WriteLine(13.14d % 5); // 3,1400000000000006

Лирическое отступление
Если заменить суффикс на f или F, то уже будет использован тип float (System.Single), который хранит числа с плавающей точкой в 32 битах в соответствии со стандартом IEEE 754. Результат получается аналогичный, отличается только ошибка округления.

Console.WriteLine(3.14f % 5); // 3,14Console.WriteLine(13.14f % 5); // 3,1400003

.NET позволяет работать с типом decimal (System.Decimal), который минимизирует ошибки округления.

Console.WriteLine(3.14m % 5); // 3,14Console.WriteLine(13.14m % 5); // 3,14

Хотя и не гарантирует их отсутствие.

Console.WriteLine(1m/3m*3m); // 0,9999999999999999999999999999


Изыди, нечистый, ибо нет в тебе истины!


В следующем примере условие оказывается истинным.

const x = { i: 1, toString: function() { return this.i++; } };if (x == 1 && x == 2 && x == 3)    document.write("This will be printed!");

Это достигается за счёт того, что при проверке на равенство операнды приводятся к одному типу. При этом на x вызывается переопределённый метод toString(), который помимо того, что что-то возвращает, изменяет состояние объекта. Отмечаем про себя, что чистые функции это прекрасно, но ради интереса пробуем воплотить концепцию из примера.

Trickster x = new Trickster();if (x == 1 && x == 2 && x == 3)    Console.WriteLine("This will be printed!");

.NET позволяет переопределять методы и приводит типы, где возможно, если соответствующее приведение определено.

class Trickster{    private int value = 1;    public override string ToString() =>        value++.ToString();    public static implicit operator int(Trickster trickster) =>        int.Parse(trickster.ToString());}

Ура, работает. Но смущает, что Trickster как будто подстраивается под int. Заменим

public static implicit operator int(Trickster trickster) =>    int.Parse(trickster.ToString());

на

public static implicit operator double(Trickster trickster) =>    double.Parse(trickster.ToString());

Всё равно работает. Теперь Trickster и int приводятся к double.

На самом деле, переопределение ToString() не принципиально для достижения конечного результата и сделано для максимального подражания примеру на JavaScript. Достаточно определить приведение к double. Конечно не забывая о важности побочного эффекта.

class Trickster{    private int value = 1;    public static implicit operator double(Trickster trickster) =>        trickster.value++;}

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

Trickster x = new Trickster();if (x == 1 && x == 2 && x == 3 && x == new[] { 1, 2, 3 } &&    x != 1 && x != 2 && x != 3 && x != new[] { 1, 2, 3 })    Console.WriteLine("This will be printed!");

таким нехитрым способом

class Trickster{    public static bool operator ==(Trickster trickster, object o) =>        true;    public static bool operator !=(Trickster trickster, object o) =>        true;}

Элегантный захват


Этот код на JavaScript три раза выводит 3.

for (i = 1; i <= 2; ++i)  setTimeout(() => console.log(i), 0);

Поскольку в C# функции setTimeout из коробки нет, реализуем аналог самостоятельно и получаем точно такой же результат.

void SetTimeout(Action action, int delay) =>    Task.Run(async () =>    {        await Task.Delay(delay);        action();    });for (int i = 0; i <= 2; ++i)    SetTimeout(() => Console.WriteLine(i), 0);// Не даём приложению закончить работу до завершения задачConsole.ReadKey();

Эффект от захвата переменных не очевиден, если не знать, как этот самый захват реализован. Кстати, вопросы на эту тему весьма популярны на собеседованиях дотнетчиков. По моему опыту примерно на каждом пятом спрашивают.

Комутативность никто не обещал


Операторы в JavaScript иногда не отличаются коммутативностью.

Date() && {property: 1}; // {property: 1}{property: 1} && Date(); // Uncaught SyntaxError: Unexpected token '&&'

В C# к пользовательским типам по умолчанию операторы вообще неприменимы (исключение == и != для ссылочных типов). Но некоторые из них могут быть перегружены явно в типе. Тогда забота о комутативности ложится на плечи разработчика.

class Trickster{    // В C# перегружать && недопустимо    public static object operator +(Trickster trickster, object o) =>        null;}

И она не обязательно будет реализована.

var a = new Trickster() + new object(); // OK// Compilation Error:// Operator '+' cannot be applied to operands of type 'object' and 'Trickster'var b = new object() + new Trickster();

Скрещиваем ужа с ежом


В JavaScript можно выполнять математические операции совместно над строками и числами.

var a = "41";a += 1; // "411"var b = "41";b -=- 1; // 42

В C# так можно только со сложением.

var a = "41" + 1; // 411// Compilation Error:// Operator '-' cannot be applied to operands of type 'string' and 'int'var b = "41" - (-1);

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

Trickster a = "41";Console.WriteLine(a += 1); // 411Trickster b = "41";Console.WriteLine(b -=- 1); // 42

Нужно просто реализовать пару неявных преобразований типов и перегрузить пару операторов.

class Trickster{    private string value;    public static implicit operator Trickster(string s) =>        new Trickster { value = s };    public static implicit operator Trickster(int i) =>        new Trickster { value = i.ToString() };    public static string operator +(Trickster trickster, int i) =>        trickster.value + i;    public static int operator -(Trickster trickster, int i) =>        int.Parse(trickster.value) - i;    public override string ToString() =>        value;}

Можно заменить перегрузку сложения

public static string operator +(Trickster trickster, int i) =>    trickster.value + i;

на неявное приведение к строке

public static implicit operator string(Trickster trickster) =>    trickster.value;

и получить тот же результат.

Если не заморачиваться с возвращением числа из операции вычитания и помещением этого числа в Trickster, то можно заменить

public static int operator -(Trickster trickster, int i) =>    int.Parse(trickster.value) - i;

на

public static string operator -(Trickster trickster, int i) =>    (int.Parse(trickster.value) - i).ToString();

а приведение int к Trickster удалить.

Жонглируем бананами


В JavaScript строку banana можно получить следующим способом:

('b' + 'a' + + 'a' + 'a').toLowerCase(); // "banana"('b'+'a'++'a'+'a').toLowerCase(); // Uncaught SyntaxError: Invalid left-hand side expression in postfix operation

Здесь применение унарного + ко второй 'a' возвращает NaN, который приводится к строке 'NaN' для сложения с остальными строками, и в итоге получается 'baNaNa', а для красоты всё приводится к нижнему регистру.

В C# создаем класс

class Trickster{    private string value;    public static implicit operator Trickster(string s) =>        new Trickster { value = s };    public static double operator +(Trickster trickster) =>        double.TryParse(trickster.value, out double result) ? result : double.NaN;}

и пробуем

// чтобы double.NaN.ToString() возвращал "NaN", а не "нечисло"Thread.CurrentThread.CurrentCulture = new CultureInfo("en-us");Console.WriteLine(("b" + "a" + + (Trickster)"a" + "a").ToLower());

К сожалению, несмотря на неявное приведение компилятор не может построить цепочку string -> Trickster -> double -> string, и приходится явно ему подсказывать. (Если задуматься, такое приведение выглядело бы более чем странно.)

Trickster можно реализовать иначе:

class Trickster{    private double value;    public static implicit operator Trickster(string s) =>        new Trickster { value = double.TryParse(s, out var d) ? d : double.NaN };    public static double operator +(Trickster trickster) =>        +trickster.value;}

В этом случае можно даже заменить перегрузку унарного + неявным приведением к double:

public static implicit operator double(Trickster trickster) =>    trickster.value;

В качестве бонуса, если не поставить пробел между двумя плюсами, то как и в примере на JavaScript мы получим ошибку компиляции, да не одну, а целый букет.

Лирическое отступление
Компилятор таки может строить цепочки неявный преобразований типов вида встроенное
-> [встроенное->] пользовательское
. Например, пусть есть тип

class Trickster{    public static implicit operator Trickster(long? i) =>        new Trickster();    public static Trickster operator +(Trickster left, Trickster right) =>        new Trickster();}

Тогда для выражения

var result = 0u + new Trickster();

Будет выполнена цепочка преобразований типов uint -> long -> long? -> Trickster.

Мораль


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

Внедрение зависимостей проще, чем кажется?

27.06.2020 14:19:52 | Автор: admin
Привет, Хабр!

У нас готовится к выходу второе издание легендарной книги Марка Симана, Внедрение зависимостей на платформе .NET



Поэтому сегодня мы решили кратко освежить тему внедрения зависимостей для специалистов по .NET и C# и предлагаем перевод статьи Грэма Даунса, где эта парадигма рассматривается в контексте инверсии управления (IoC) и использования контейнеров

Большинству программистов, пожалуй, известен феномен Внедрение зависимостей (Dependency Injection), но не всем понятно, какой смысл в него вкладывается. Вероятно, вам приходилось иметь дело с интерфейсами и контейнерами, и иногда работа с ними приводила вас в тупик. С другой стороны, вы, возможно, только что-то слышали о внедрении зависимостей, и открыли эту статью, так как хотите лучше разобраться в их сути. В этой статье я покажу, насколько проста концепция внедрения зависимостей, и какой мощной она может быть на самом деле.

Внедрение зависимостей это самодостаточный подход, который можно использовать сам по себе. С другой стороны, этот подход можно применять и вместе с интерфейсами, и с контейнерами для внедрения зависимостей/инверсии управления (DI/IoC). Применяя внедрение зависимостей в таком контексте, можно столкнуться с некоторой путаницей, которую поначалу испытывал и я.

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

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

(Здесь важно отметить: интерфейсы и контейнеры используются только в контексте внедрения зависимостей. Внедрение зависимостей можно реализовать и без интерфейсов/контейнеров, но, в сущности, единственное назначение интерфейсов или контейнеров облегчить внедрение зависимостей).

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

Подготовка


Чтобы лучше понять внедрение зависимостей в их чистейшей форме, давайте разберем пример приложения, написанного на C#.

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

По ходу работы вы увидите, как требования, предъявляемые к логированию, постепенно усложняются, и мы удовлетворяем эти требования, используя внедрение зависимостей; при этом зона ответственности класса Calculator сводится к минимуму. Внедрение зависимостей также избавит нас от необходимости видоизменять класс Calculator всякий раз, когда мы захотим поменять устройство логирования.

Приложение


Рассмотрим следующий код. Он написан для простого приложения-калькулятора, принимающего два числа, оператор и выводящего результат. (Это простое рабочее приложение для командной строки, поэтому вам не составит труда воспроизвести его как C# Console Application в Visual Studio и вставить туда код, если вы хотите следить за развитием примера. Все должно работать без проблем.)

У нас есть класс Calculator и основной класс Program, использующий его.

Program.cs:

using System;using System.Linq;namespace OfferZenDiTutorial{    class Program    {        static void Main(string[] args)        {            var number1 = GetNumber("Enter the first number: > ");            var number2 = GetNumber("Enter the second number: > ");            var operation = GetOperator();            var calc = new Calculator();            var result = GetResult(calc, number1, number2, operation);            Console.WriteLine($"{number1} {operation} {number2} = {result}");            Console.Write("Press any key to continue...");            Console.ReadKey();        }        private static float GetNumber(string message)        {            var isValid = false;            while (!isValid)            {                Console.Write(message);                var input = Console.ReadLine();                isValid = float.TryParse(input, out var number);                if (isValid)                    return number;                Console.WriteLine("Please enter a valid number. Press ^C to quit.");            }            return -1;        }        private static char GetOperator()        {            var isValid = false;            while (!isValid)            {                Console.Write("Please type the operator (/*+-) > ");                var input = Console.ReadKey();                Console.WriteLine();                var operation = input.KeyChar;                if ("/*+-".Contains(operation))                {                    isValid = true;                    return operation;                }                Console.WriteLine("Please enter a valid operator (/, *, +, or -). " +                                  "Press ^C to quit.");            }            return ' ';        }        private static float GetResult(Calculator calc, float number1, float number2,             char operation)        {            switch (operation)            {                case '/': return calc.Divide(number1, number2);                case '*': return calc.Multiply(number1, number2);                case '+': return calc.Add(number1, number2);                case '-': return calc.Subtract(number1, number2);                default:                    // Такого произойти не должно, если с предыдущими валидациями все было нормально                     throw new InvalidOperationException("Invalid operation passed: " +                                                         operation);            }        }    }}

Главная программа запускается, запрашивает у пользователя два числа и оператор, а затем вызывает класс Calculator для выполнения простой арифметической операции над этими числами. Затем выводит результат операции. Вот класс Calculator.

Calculator.cs:

namespace OfferZenDiTutorial{    public class Calculator    {        public float Divide(float number1, float number2)        {            return number1 / number2;        }        public float Multiply(float number1, float number2)        {            return number1 * number2;        }        public float Add(float number1, float number2)        {            return number1 + number2;        }        public float Subtract(float number1, float number2)        {            return number1 - number2;        }    }}

Логирование


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

Кажется, что не так это и сложно, верно? Берете и добавляете инструкции, в соответствии с которыми все операции, производимые в Calculator, должны заноситься в текстовый файл. Вот как теперь выглядит ваш Calculator:

Calculator.cs:

using System.IO;namespace OfferZenDiTutorial{    public class Calculator    {        private const string FileName = "Calculator.log";        public float Divide(float number1, float number2)        {            File.WriteAllText(FileName, $"Running {number1} / {number2}");            return number1 / number2;        }        public float Multiply(float number1, float number2)        {            File.WriteAllText(FileName, $"Running {number1} * {number2}");            return number1 * number2;        }        public float Add(float number1, float number2)        {            File.WriteAllText(FileName, $"Running {number1} + {number2}");            return number1 + number2;        }        public float Subtract(float number1, float number2)        {            File.WriteAllText(FileName, $"Running {number1} - {number2}");            return number1 - number2;        }    }}

Прекрасно работает. Всякий раз, когда в Calculator что-либо происходит, он записывает это в файл Calculator.log, расположенный в той же директории, откуда он запускается.

Но, возможен вопрос: а в самом ли деле уместно, чтобы класс Calculator отвечал за запись в текстовый файл?

Класс FileLogger


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

Первым делом создаем совершенно новый класс, назовем его FileLogger. Вот как он будет выглядеть.

FileLogger.csh:

using System;using System.IO;namespace OfferZenDiTutorial{    public class FileLogger    {        private const string FileName = "Calculator.log";        private readonly string _newLine = Environment.NewLine;        public void WriteLine(string message)        {            File.AppendAllText(FileName, $"{message}{_newLine}");        }    }}

Теперь все, что касается создания файла логов и записи информации в него обрабатывается в этом классе. Дополнительно получаем и одну приятную мелочь: что бы ни потреблял этот класс, не требуется ставить между отдельными записями пустые строки. Записи должны просто вызывать наш метод WriteLine, а все остальное мы берем на себя. Разве не круто?
Чтобы использовать класс, нам нужен объект, который его инстанцирует. Давайте решим эту проблему внутри класса Calculator. Заменим содержимое класса Calculator.cs следующим:

Calculator.cs:

namespace OfferZenDiTutorial{    public class Calculator    {        private readonly FileLogger _logger;        public Calculator()        {            _logger = new FileLogger();        }        public float Divide(float number1, float number2)        {            _logger.WriteLine($"Running {number1} / {number2}");            return number1 / number2;        }        public float Multiply(float number1, float number2)        {            _logger.WriteLine($"Running {number1} * {number2}");            return number1 * number2;        }        public float Add(float number1, float number2)        {            _logger.WriteLine($"Running {number1} + {number2}");            return number1 + number2;        }        public float Subtract(float number1, float number2)        {            _logger.WriteLine($"Running {number1} - {number2}");            return number1 - number2;        }    }}

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

Внедрение зависимости


Очевидно, ответ на последний вопрос отрицательный!

Именно здесь, уважаемый читатель, в дело вступает внедрение зависимости. Давайте изменим конструктор нашего класса Calculator:

Calculator.cs:

        public Calculator(FileLogger logger)        {            _logger = logger;        }

Вот и все. Больше в классе ничего не меняется.

Внедрение зависимостей это элемент более крупной темы под названием Мнверсия управления, но ее подробное рассмотрение выходит за рамки этой статьи.

В данном случае вам всего лишь требуется знать, что мы инвертируем управление классом логгера, либо, выражаясь метафорически, делегируем кому-то проблему создания файла FileLogger, внедряя экземпляр FileLogger в наш калькулятор, а не рассчитывая, что класс Calculator сам будет знать, как его создать.

Итак, чья же это ответственность?

Как раз того, кто инстанцирует класс Calculator. В нашем случае это основная программа.

Чтобы это продемонстрировать, изменим метод Main в нашем классе Program.cs следующим образом:

Program.cs

  static void Main(string[] args)        {            var number1 = GetNumber("Enter the first number: > ");            var number2 = GetNumber("Enter the second number: > ");            var operation = GetOperator();            // Следующие две строки изменены            var logger = new FileLogger();            var calc = new Calculator(logger);            var result = GetResult(calc, number1, number2, operation);            Console.WriteLine($"{number1} {operation} {number2} = {result}");            Console.Write("Press any key to continue...");            Console.ReadKey();        }

Таким образом, требуется изменить всего две строки. Мы не рассчитываем, что класс Calculator инстанцирует FileLogger, это за него сделает Main, а затем передаст ему результат.

В сущности, это и есть внедрение зависимостей. Не нужны ни интерфейсы, ни контейнеры для инверсии управления, ни что-либо подобное. В принципе, если вам доводилось выполнять что-либо подобное, то вы имели дело с внедрением зависимостей. Круто, правда?

Расширение возможностей: сделаем другой логгер


Несмотря на вышесказанное, у интерфейсов есть свое место, и по-настоящему они раскрываются именно в связке с Внедрением Зависимостей.

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

Как вы считаете, придется ли ради этого делать изменения внутри Calculator, что потенциально потребует перекомпилировать и перераспределить ту сборку, в которой он находится?

Вот здесь нам и пригодятся интерфейсы.

Давайте напишем интерфейс. Назовем его ILogger, поскольку его реализацией будет заниматься наш класс FileLogger.

ILogger.cs

namespace OfferZenDiTutorial{    public interface ILogger    {        void WriteLine(string message);    }}

Как видите, он определяет единственный метод: WriteLine, реализованный FileLogger. Сделаем еще шаг и формализуем эти отношения, сделав так, чтобы этот класс официально реализовывал наш новый интерфейс:

FileLogger.cs

public class FileLogger : ILogger

Это единственное изменение, которое мы внесем в этот файл. Все остальное будет как прежде.
Итак, отношение мы определили что нам теперь с ним делать?

Для начала изменим класс Calculator таким образом, чтобы он использовал интерфейс ILogger, а не конкретную реализацию FileLogger:

Calculator.cs

private readonly ILogger _logger;        public Calculator(ILogger logger)        {            _logger = logger;        }

На данном этапе код по-прежнему компилируется и выполняется без всяких проблем. Мы передаем в него FileLogger из главного метода программы, того, который реализует ILogger. Единственное отличие заключается в том, что Calculator не просто не требуется знать, как создавать FileLogger, но и даже логгер какого рода ему выдается.

Поскольку все, что бы вы ни получили, реализует интерфейс ILogger (и, следовательно, имеет метод WriteLine), с практическим использованием проблем не возникает.

Теперь давайте добавим еще одну реализацию интерфейса ILogger. Это будет класс, который ничего не делает при вызове метода WriteLine. Мы назовем его NullLogger, и вот как он выглядит:

NullLogger.cs

namespace OfferZenDiTutorial{    public class NullLogger : ILogger    {        public void WriteLine(string message)        {            // Ничего не делаем в этой реализации        }    }}

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

Нам потребуется изменить только лишь метод Main в нашем файле Program.cs, чтобы передать в него иную реализацию. Давайте этим и займемся, чтобы метод Main принял следующий вид:

Program.cs

 static void Main(string[] args)        {            var number1 = GetNumber("Enter the first number: > ");            var number2 = GetNumber("Enter the second number: > ");            var operation = GetOperator();            var logger = new NullLogger(); // Эту строку нужно изменить            var calc = new Calculator(logger);            var result = GetResult(calc, number1, number2, operation);            Console.WriteLine($"{number1} {operation} {number2} = {result}");            Console.Write("Press any key to continue...");            Console.ReadKey();        }

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

Небольшая оговорка об интерфейсах


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

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

Контейнеры для внедрения зависимостей


Пример, прорабатываемый в этой статье, довольно прост: мы имеем дело с единственным классом, у которого всего одна зависимость. Теперь предположим, что у нас множество зависимостей, и каждая из них связана с другими зависимостями. Даже при работе над умеренно сложными проектами вполне вероятно, что вам придется иметь дело с такими ситуациями, и будет не просто держать в уме, что нужно создать все эти классы, а также помнить, какой из них от какого зависит особенно, если вы решили пользоваться интерфейсами.

Знакомьтесь с контейнером для внедрения зависимостей. Он упрощает вам жизнь, но принцип работы такого контейнера может показаться весьма запутанным, особенно, когда вы только начинаете его осваивать. На первый взгляд эта возможность может отдавать некоторой магией.
В данном примере мы воспользуемся контейнером от Unity, но на выбор есть и много других, назову лишь наиболее популярные: Castle Windsor, Ninject. С функциональной точки зрения эти контейнеры практически не отличаются. Разница может быть заметна на уровне синтаксиса и стиля, но, в конечном итоге, все сводится к вашим персональным предпочтениям и опыту разработки (а также к тому, что предписывается в вашей компании!).

Давайте подробно разберем пример с использованием Unity: я постараюсь объяснить, что здесь происходит.

Первым делом вам потребуется добавить ссылку на Unity. К счастью, для этого существует пакет Nuget, поэтому щелкните правой кнопкой мыши по вашему проекту в Visual Studio и выберите Manage Nuget Packages:



Найдите и установите пакет Unity, ориентируйтесь на проект Unity Container:



Итак, мы готовы. Измените метод Main файла Program.cs вот так:

Program.cs

 static void Main(string[] args)        {            var number1 = GetNumber("Enter the first number: > ");            var number2 = GetNumber("Enter the second number: > ");            var operation = GetOperator();            // Следующие три строки необходимо изменить            var container = new UnityContainer();            container.RegisterType<ILogger, NullLogger>();            var calc = container.Resolve<Calculator>();            var result = GetResult(calc, number1, number2, operation);            Console.WriteLine($"{number1} {operation} {number2} = {result}");            Console.Write("Press any key to continue...");            Console.ReadKey();        }

Опять же, менять требуется только те строки, которые отмечены. При этом, нам не придется ничего менять ни в классе Calculator, ни в одном из логгеров, ни в их интерфейсе: теперь они все внедряются во время исполнения, поэтому, если нам понадобится иной логгер, то мы должны будем всего лишь изменить конкретный класс, регистрируемый для ILogger.

При первом запуске этого кода вы можете столкнуться с такой ошибкой:



Вероятно, это одна из причуд с той версией пакета Unity, которая была актуальна на момент написания этой статьи. Надеюсь, что у вас все пройдет гладко.
Все дело в том, что при установке Unity также устанавливается неверная версия другого пакета, System.Runtime.CompilerServices.Unsafe. Если вы получаете такую ошибку, то должны вернуться к менеджеру пакетов Nuget, найти этот пакет под вкладкой Installed и обновить его до новейшей стабильной версии:



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

Все начинается со строки var calc = container.Resolve<Calculator>();, поэтому именно отсюда я изложу смысл этого кода в форме диалога контейнера с самим собой: о чем он думает, когда видит эту инструкцию.

  1. Мне задано разрешить что-то под названием Calculator. Я знаю, что это такое?
  2. Вижу, в актуальном дереве процессов есть класс под названием Calculator. Это конкретный тип, значит, у него всего лишь одна реализация. Просто создам экземпляр этого класса. Как выглядят конструкторы?
  3. Хм, а конструктор всего один, и принимает он что-то под названием ILogger. Я знаю, что это такое?
  4. Нашел, но это же интерфейс. Мне вообще сообщалось, как его разрешать?
  5. Да, сообщалось! В предыдущей строке сказано, что, всякий раз, когда мне требуется разрешить ILogger, я должен передать экземпляр класса NullLogger.
  6. Окей, значит тут есть NullLogger. У него непараметризованный конструктор. Просто создам экземпляр.
  7. Передам этот экземпляр конструктору класса Calculator, а затем верну этот экземпляр к var calc.

Обратите внимание: если бы у NullLogger был конструктор, который запрашивал бы дополнительные типы, то конструктор просто повторил бы для них все шаги, начиная с 3. В принципе, он просматривает все типы и пытается автоматически разрешить все найденные типы в конкретные экземпляры.

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

Вот и все. Ничего таинственного и особо мистического.

Другие возможности


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

Если вы хотите сами поэкспериментировать с примерами, приведенными в этой статье: смело клонируйте с Гитхаба репозиторий, в котором они выложены github.com/GrahamDo/OfferZenDiTutorial.git. Там семь веток, по одной на каждую рассмотренную нами итерацию.
Подробнее..

Umka. Жизнь статической типизации в скриптовом языке

21.06.2020 14:07:34 | Автор: admin


В своё время посты на Хабре и Reddit о статически типизированном скриптовом языке Umka вызвали весьма активную дискуссию.

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

Появились первые замеры быстродействия интерпретатора в сравнении с Wren и Python их результаты внушают оптимизм. Наконец, родился более реалистичный пример использования Umka по его основному назначению, т. е. как встраиваемого языка.

Информация о типах во время исполнения программы (RTTI). Проект начинался с радикального отказа от хранения типов данных при исполнении программы. Вся информация о типах терялась после компиляции в байт-код виртуальной машины. В принципе, статическая типизация позволяет это сделать, а заодно избавляет от странных трюков вроде упаковки данных в NaN, к которой, например, прибегают создатели JavaScript и Wren ради увеличения быстродействия. Однако обнаружились два случая, в которых пришлось использовать RTTI:

  • Приведение интерфейсного типа данных к конкретному прямой аналог утверждения типа (type assertion) в Go, а также, отчасти, оператора dynamic_cast в C++. Оно требуется и при сборке мусора, содержащегося в данных, приведённых к интерфейсному типу.
  • Сборка мусора, связанного с динамическими структурами данных вроде списков и деревьев.

Быстродействие. Изначально Umka никак не предназначался для установления рекордов быстродействия. Безразличие публики к медлительности Python наводило на мысль, что скорость вовсе не то качество, которого в первую очередь ожидают от скриптового языка. Однако успех LuaJIT и активная реклама Wren заставили задуматься. После этого меня уже не удивляло, что и ранние публикации про Umka вызвали вопросы о быстродействии, хотя мне по-прежнему интересно, от кого в первую очередь исходит спрос на скорость. От разработчиков игр?

Пока полный набор тестов не готов, я могу поделиться лишь предварительными результатами замеров. В численных задачах (например, задаче многих тел) Umka надёжно опережает Python, а если в задаче активно используется цикл for, то Umka даёт выигрыш даже по сравнению с Wren, который позиционируется автором чуть ли не как самый быстрый скриптовый язык после LuaJIT. Наглядным примером служит перемножение больших матриц:


Умножение матриц 400 x 400 (AMD A4-3300M @ 1.9 GHz, Windows 7)

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

Задачи с интенсивной сборкой мусора (например, создание и обход двоичных деревьев) вызывают много сомнений по поводу эквивалентности сравниваемых алгоритмов. Например, известная реализация двоичных деревьев на Python возвращает содержимое узлов россыпью и выглядит так, будто в принципе допускает размещение всего дерева на стеке вообще без использования кучи и сборки мусора. Однако она, по-видимому, требует динамической типизации и не может быть точно воспроизведена на Umka. Если же потребовать возвращать узлы в виде структур, как в Umka (а за неимением структур приходится требовать объекты), то быстродействие Python сразу же падает в 3-4 раза. Вариант на Umka вдвое отстаёт от первой реализации и вдвое опережает вторую. Какое сравнение корректнее не знаю.

Взаимодействие с внешним кодом. Коль скоро язык рассматривается как встраиваемый, понадобился более или менее реалистичный пример взаимодействия кода на C и Umka. В нём средствами игровой библиотеки raylib формируется трёхмерная сцена, а наполнение сцены определяется внешним скриптом на Umka. В примере можно найти и вызов функций Umka из кода на C, и вызов функций C из Umka. Статическая типизация языка Umka позволила естественным образом формировать на нём структуры данных, непосредственно воспринимаемые библиотекой raylib.


Пример трёхмерной сцены, содержимое которой задаётся скриптом на Umka

Обобщённые типы и функции (generics). Как только читатель улавливает сходство Umka с Go, пускай даже синтаксическое следует вопрос о поддержке generic'ов. Работа в этом направлении пока не вышла из стадии обзора подходов. Конечно, хотелось бы воспользоваться предложениями разработчиков Go, однако сосуществование в их головах интерфейсов и контрактов всегда отпугивало, как странное дублирование понятий. К удивлению и радости, в только что вышедшей новой редакции черновика контракты исчезли по тем же причинам, о которых размышлял и я. Пока generic'ов в Umka нет, остаётся пользоваться, как и в Go, пустыми интерфейсами interface{}.

Документация. Полная спецификация Umka ещё в работе, но уже написана грамматика и расширен обзорный тур по основным возможностям языка.
Подробнее..

Перевод Магические сигнатуры методов в C

01.07.2020 14:11:33 | Автор: admin

Представляю вашему вниманию перевод статьи The Magical Methods in C# автора CEZARY PITEK.


Есть определенный набор сигнатур методов в C#, имеющих поддержку на уровне языка. Методы с такими сигнатурами позволяют использовать специальный синтаксис со всеми его преимуществами. Например, с их помощью можно упростить наш код или создать DSL для того, чтобы выразить решение проблемы более красивым образом. Я встречаюсь с такими методами повсеместно, так что я решил написать пост и обобщить все мои находки по этой теме, а именно:


  • Синтаксис инициализации коллекций
  • Синтаксис инициализации словарей
  • Деконструкторы
  • Пользовательские awaitable типы
  • Паттерн query expression

Синтаксис инициализации коллекций


Синтаксис инициализации коллекции довольно старая фича, т. к. она существует с C# 3.0 (выпущен в конце 2007 года). Напомню, синтаксис инициализации коллекции позволяет создать список с элементами в одном блоке:


var list = new List<int> { 1, 2, 3 };

Этот код эквивалентен приведенному ниже:


var list = new List<int>();list.Add(1);list.Add(2);list.Add(3);

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


  • тип имплементирует интерфейс IEnumerable
  • тип имеет метод с сигнатурой void Add(T item)

public class CustomList<T>: IEnumerable{    public IEnumerator GetEnumerator() => throw new NotImplementedException();    public void Add(T item) => throw new NotImplementedException();}

Мы можем добавить поддержку синтаксиса инициализации коллекции, определив Add как метод расширения:


public static class ExistingTypeExtensions{    public static void Add<T>(ExistingType @this, T item) => throw new NotImplementedException();}

Этот синтаксис также можно использовать для вставки элементов в поле-коллекцию без публичного сеттера:


class CustomType{    public List<string> CollectionField { get; private set; }  = new List<string>();}class Program{    static void Main(string[] args)    {        var obj = new CustomType        {            CollectionField =            {                "item1",                "item2"            }        };    }}

Синтаксис инициализации коллекции полезен при инициализации коллекции известным числом элементов. Но что если мы хотим создать коллекцию с переменным числом элементов? Для этого есть менее известный синтаксис:


var obj = new CustomType{    CollectionField =    {        { existingItems }    }};

Такое возможно для типов, удовлетворяющих следующим условиям:


  • тип имплементирует интерфейс IEnumerable
  • тип имеет метод с сигнатурой void Add(IEnumerable<T> items)

public class CustomList<T>: IEnumerable{    public IEnumerator GetEnumerator() => throw new NotImplementedException();    public void Add(IEnumerable<T> items) => throw new NotImplementedException();}

К сожалению, массивы и коллекции из BCL не реализуют метод void Add(IEnumerable<T> items), но мы можем изменить это, определив метод расширения для существующих типов коллекций:


public static class ListExtensions{    public static void Add<T>(this List<T> @this, IEnumerable<T> items) => @this.AddRange(items);}

Благодаря этому мы можем написать следующее:


var obj = new CustomType{    CollectionField =    {        { existingItems.Where(x => /*Filter items*/).Select(x => /*Map items*/) }    }};

Или даже собрать коллекцию из смеси индивидуальных элементов и результатов нескольких перечислений (IEnumerable):


var obj = new CustomType{    CollectionField =    {        individualElement1,        individualElement2,        { list1.Where(x => /*Filter items*/).Select(x => /*Map items*/) },        { list2.Where(x => /*Filter items*/).Select(x => /*Map items*/) },    }};

Без подобного синтаксиса очень сложно получить подобный результат в блоке инициализации.


Я узнал об этой фиче совершенно случайно, когда работал с маппингами для типов с полями-коллекциями, сгенерированными из контрактов protobuf. Для тех, кто не знаком с protobuf: если вы используете grpctools для генерации типов .NET из файлов proto, все поля-коллекции генерируются подобным образом:


[DebuggerNonUserCode]public RepeatableField<ItemType> SomeCollectionField{    get    {        return this.someCollectionField_;    }}

Как можно заметить, поля-коллекции не имеют сеттер, но RepeatableField реализует метод void Add(IEnumerable items), так что мы по-прежнему можем инициализировать их в блоке инициализации:


/// <summary>/// Adds all of the specified values into this collection. This method is present to/// allow repeated fields to be constructed from queries within collection initializers./// Within non-collection-initializer code, consider using the equivalent <see cref="AddRange"/>/// method instead for clarity./// </summary>/// <param name="values">The values to add to this collection.</param>public void Add(IEnumerable<T> values){    AddRange(values);}

Синтаксис инициализации словарей


Одна из крутых фич C# 6.0 инициализация словаря по индексу, которая упростила синтаксис инициализации словарей. Благодаря ей мы можем писать более читаемый код:


var errorCodes = new Dictionary<int, string>{    [404] = "Page not Found",    [302] = "Page moved, but left a forwarding address.",    [500] = "The web server can't come out to play today."};

Этот код эквивалентен следующему:


var errorCodes = new Dictionary<int, string>();errorCodes[404] = "Page not Found";errorCodes[302] = "Page moved, but left a forwarding address.";errorCodes[500] = "The web server can't come out to play today.";

Это немного, но это определенно упрощает написание и чтение кода.


Лучшее в инициализации по индексу это то, что она не ограничивается классом Dictionary<T> и может быть использована с любым другим типом, определившим индексатор:


class HttpHeaders{    public string this[string key]    {        get => throw new NotImplementedException();        set => throw new NotImplementedException();    }}class Program{    static void Main(string[] args)    {        var headers = new HttpHeaders        {            ["access-control-allow-origin"] = "*",            ["cache-control"] = "max-age=315360000, public, immutable"        };    }}

Деконструкторы


В C# 7.0 помимо кортежей был добавлен механизм деконструкторов. Они позволяют декомпозировать кортеж в набор отдельных переменных:


var point = (5, 7);// decomposing tuple into separated variablesvar (x, y) = point;

Что эквивалентно следующему:


ValueTuple<int, int> point = new ValueTuple<int, int>(1, 4);int x = point.Item1;int y = point.Item2;

Этот синтаксис позволяет обменять значения двух переменных без явного объявления третьей:


int x = 5, y = 7;//switch(x, y) = (y,x);

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


class Point{    public int X { get; }    public int Y { get; }    public Point(int x, int y)  => (X, Y) = (x, y);}

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


  • метод называется Deconstruct
  • метод возвращает void
  • все параметры метода имеют модификатор out

Для нашего типа Point мы можем объявить деконструктор следующим образом:


class Point{    public int X { get; }    public int Y { get; }    public Point(int x, int y) => (X, Y) = (x, y);    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);}

Пример использования приведен ниже:


var point = new Point(2, 4);var (x, y) = point;

"Под капотом" он превращается в следующее:


int x;int y;new Point(2, 4).Deconstruct(out x, out y);

Деконструкторы могут быть добавлены к типам с помощью методов расширения:


public static class PointExtensions{     public static void Deconstruct(this Point @this, out int x, out int y) => (x, y) = (@this.X, @this.Y);}

Один из самых полезных примеров применения деконструкторов это деконструкция KeyValuePair<TKey, TValue>, которая позволяет с легкостью получить доступ к ключу и значению во время итерирования по словарю:


foreach (var (key, value) in new Dictionary<int, string> { [1] = "val1", [2] = "val2" }){    //TODO: Do something}

KeyValuePair<TKey, TValue>.Deconstruct(TKey, TValue) доступно только с netstandard2.1. Для предыдущих версий netstandard нам нужно использовать ранее приведенный метод расширения.


Пользовательские awaitable типы


C# 5.0 (выпущен вместе с Visual Studio 2012) ввел механизм async/await, который стал переворотом в области асинхронного программирования. Прежде вызов асинхронного метода представлял собой запутанный код, особенно когда таких вызовов было несколько:


void DoSomething(){    DoSomethingAsync().ContinueWith((task1) => {        if (task1.IsCompletedSuccessfully)        {            DoSomethingElse1Async(task1.Result).ContinueWith((task2) => {                if (task2.IsCompletedSuccessfully)                {                    DoSomethingElse2Async(task2.Result).ContinueWith((task3) => {                        //TODO: Do something                    });                }            });        }    });}private Task<int> DoSomethingAsync() => throw new NotImplementedException();private Task<int> DoSomethingElse1Async(int i) => throw new NotImplementedException();private Task<int> DoSomethingElse2Async(int i) => throw new NotImplementedException();

Это может быть переписано намного красивее с использованием синтаксиса async/await:


async Task DoSomething(){    var res1 = await DoSomethingAsync();    var res2 = await DoSomethingElse1Async(res1);    await DoSomethingElse2Async(res2);}

Это может прозвучать удивительно, но ключевое слово await не зарезервировано только под использование с типом Task. Оно может быть использовано с любым типом, который имеет метод GetAwaiter, возвращающий удовлетворяющий следующим требованиям тип:


  • тип имплементирует интерфейс System.Runtime.CompilerServices.INotifyCompletion и реализует метод void OnCompleted(Action continuation)
  • тип имеет свойство IsCompleted логического типа
  • тип имеет метод GetResult без параметров

Для добавления поддержки ключевого слова await к пользовательскому типу мы должны определить метод GetAwaiter, возвращающий TaskAwaiter<TResult> или пользовательский тип, удовлетворяющий приведенным выше условиям:


class CustomAwaitable{    public CustomAwaiter GetAwaiter() => throw new NotImplementedException();}class CustomAwaiter: INotifyCompletion{    public void OnCompleted(Action continuation) => throw new NotImplementedException();    public bool IsCompleted => throw new NotImplementedException();    public void GetResult() => throw new NotImplementedException();}

Вы можете спросить: "Каков возможный сценарий использования синтаксиса await с пользовательским awaitable типом?". Если это так, то я рекомендую вам прочитать статью Stephen Toub под названием "await anything", которая показывает множество интересных примеров.


Паттерн query expression


Лучшее нововведение C# 3.0 Language-Integrated Query, также известное как LINQ, предназначенное для манипулирования коллекциями с SQL-подобным синтаксисом. LINQ имеет две вариации: SQL-подобный синтаксис и синтаксис методов расширения. Я предпочитаю второй вариант, т. к. по моему мнению он более читаем, а также потому что я привык к нему. Интересный факт о LINQ заключается в том, что SQL-подобный синтаксис во время компиляции транслируется в синтаксис методов расширения, т. к. это фича C#, а не CLR. LINQ был разработан в первую очередь для работы с типами IEnumerable, IEnumerable<T> и IQuerable<T>, но он не ограничен только ими, и мы можем использовать его с любым типом, удовлетворяющим требованиям паттерна query expression. Полный набор сигнатур методов, используемых LINQ, таков:


class C{    public C<T> Cast<T>();}class C<T> : C{    public C<T> Where(Func<T,bool> predicate);    public C<U> Select<U>(Func<T,U> selector);    public C<V> SelectMany<U,V>(Func<T,C<U>> selector, Func<T,U,V> resultSelector);    public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,U,V> resultSelector);    public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector);    public O<T> OrderBy<K>(Func<T,K> keySelector);    public O<T> OrderByDescending<K>(Func<T,K> keySelector);    public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector);    public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector, Func<T,E> elementSelector);}class O<T> : C<T>{    public O<T> ThenBy<K>(Func<T,K> keySelector);    public O<T> ThenByDescending<K>(Func<T,K> keySelector);}class G<K,T> : C<T>{    public K Key { get; }}

Разумеется, мы не обязаны реализовывать все эти методы для того, чтобы использовать синтаксис LINQ с нашим пользовательским типом. Список обязательных операторов и методов LINQ для них можно посмотреть здесь. Действительно хорошее объяснение того, как это сделать, можно найти в статье Understand monads with LINQ автора Miosz Piechocki.


Подведение итогов


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

Подробнее..

Изучаем VoIP-движок Mediastreamer2. Часть 13, заключительная

01.07.2020 16:06:10 | Автор: admin

Материал статьи взят с моего дзен-канала.



В прошлой статье, мы рассмотрели вопросы отладки крафтовых фильтров, связанные с перемещением данных.


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


Что такое нагрузка на тикер


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


MS2_PUBLIC float ms_ticker_get_average_load(MSTicker *ticker);

Если возвращаемая величина близка к 100% это означает, что данный тикер еле поспевает сделать свою работу до начала очередного тика. Если у нас приложение, которому не требуется работа в реальном времени (например оно просто пишет звук в файл), то для нас не особо важно на какое время был отложен очередной вызов тикера. Но в реалтайм-приложении задержка обработки может повлиять на момент отправки RTP-пакета, что в свою очередь может сказаться на качестве звука или видео. В некоторых случаях влияние задержки отдельных пакетов можно купировать используя буфер пакетов на приемном конце (так называемый джиттер-буфер). При этом ваш звук будет воспроизводиться без дефектов, но с задержкой, пропорциональной длине буфера. Что может быть не приемлемо в случаях когда звуковой сигнал используется для управления процессами в реальном времени.


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


struct _MSTickerLateEvent{int lateMs; /**<Запаздывание которое было в последний раз, в миллисекундах */uint64_t time; /**< Время возникновения события, в миллисекундах */int current_late_ms; /**< Запаздывание на текущем тике, в миллисекундах */};typedef struct _MSTickerLateEvent MSTickerLateEvent;

По которому в консоль выводится сообщение, которое выглядит примерно так:


ortp-warning-MSTicker: We are late of 164 miliseconds


С помощью функции


void ms_ticker_get_last_late_tick(MSTicker *ticker, MSTickerLateEvent *ev);

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


Способы снижения загрузки тикера


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


Изменение приоритета тикера


Приоритет тикера имеет три градации, которые определены в перечислении MSTickerPrio:


enum _MSTickerPrio{MS_TICKER_PRIO_NORMAL, /* Приоритет соответствующий значению по умолчанию для данной ОС. */MS_TICKER_PRIO_HIGH, /* Увеличенный приоритет устанавливается подlinux/MacOS с помощью setpriority() или sched_setschedparams() устанавливается политика SCHED_RR. */MS_TICKER_PRIO_REALTIME /* Наибольший приоритет, для него под Linux используется политика SCHED_FIFO. */};typedef enum _MSTickerPrio MSTickerPrio;

Чтобы поэкспериментировать с нагрузкой тикера, нам требуется схема, которая во время работы будет наращивать нагрузку и завершать работу когда нагрузка достигнет уровня 99%. В качестве нагрузки будем использовать схему:
ticker -> voidsource -> dtmfgen -> voidsink
Нагрузка будет увеличиваться добавлением между dtmfgen и voidsink нового элемента управления уровнем сигнала (тип фильтра MS_VOLUME), с коэффициентом передачи неравным единице, чтобы фильтр не филонил.
Она показана на рисунке

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


Файл mstest13.c Переменная вычислительная нагрузка.
/* Файл mstest13.c Переменная вычислительная нагрузка. */#include <stdio.h>#include <signal.h>#include <mediastreamer2/msfilter.h>#include <mediastreamer2/msticker.h>#include <mediastreamer2/dtmfgen.h>#include <mediastreamer2/mssndcard.h>#include <mediastreamer2/msvolume.h>/*----------------------------------------------------------*/struct _app_vars{    int  step;              /* Количество фильтров добавляемых за раз. */    int  limit;             /* Количество фильтров на котором закончить работу. */    int  ticker_priority;   /* Приоритет тикера. */    char* file_name;        /* Имя выходного файла. */    FILE *file;};typedef struct _app_vars app_vars;/*----------------------------------------------------------*//* Функция преобразования аргументов командной строки в* настройки программы. */void  scan_args(int argc, char *argv[], app_vars *v){    char i;    for (i=0; i<argc; i++)    {        if (!strcmp(argv[i], "--help"))        {            char *p=argv[0]; p=p + 2;            printf("  %s computational load\n\n", p);            printf("--help      List of options.\n");            printf("--version   Version of application.\n");            printf("--step      Filters count per step.\n");            printf("--tprio     Ticker priority:\n"                    "            MS_TICKER_PRIO_NORMAL   0\n"                    "            MS_TICKER_PRIO_HIGH     1\n"                    "            MS_TICKER_PRIO_REALTIME 2\n");            printf("--limit     Filters count limit.\n");            printf("-o          Output file name.\n");            exit(0);        }        if (!strcmp(argv[i], "--version"))        {            printf("0.1\n");            exit(0);        }        if (!strcmp(argv[i], "--step"))        {            v->step = atoi(argv[i+1]);            printf("step: %i\n", v->step);        }        if (!strcmp(argv[i], "--tprio"))        {            int prio = atoi(argv[i+1]);            if ((prio >=MS_TICKER_PRIO_NORMAL) && (prio <= MS_TICKER_PRIO_REALTIME))            {                v->ticker_priority = atoi(argv[i+1]);                printf("ticker priority: %i\n", v->ticker_priority);            }            else            {                printf(" Bad ticker priority: %i\n", prio);                exit(1);            }        }        if (!strcmp(argv[i], "--limit"))        {            v->limit = atoi(argv[i+1]);            printf("limit: %i\n", v->limit);        }        if (!strcmp(argv[i], "-o"))        {            v->file_name=argv[i+1];            printf("file namet: %s\n", v->file_name);        }    }}/*----------------------------------------------------------*//* Структура для хранения настроек программы. */app_vars vars;/*----------------------------------------------------------*/void saveMyData(){    // Закрываем файл.    if (vars.file) fclose(vars.file);    exit(0);}void signalHandler( int signalNumber ){    static pthread_once_t semaphore = PTHREAD_ONCE_INIT;    printf("\nsignal %i received.\n", signalNumber);    pthread_once( & semaphore, saveMyData );}/*----------------------------------------------------------*/int main(int argc, char *argv[]){    /* Устанавливаем настройки по умолчанию. */    app_vars vars={100, 100500, MS_TICKER_PRIO_NORMAL, 0};    // Подключаем обработчик Ctrl-C.    signal( SIGTERM, signalHandler );    signal( SIGINT,  signalHandler );    /* Устанавливаем настройки настройки программы в     * соответствии с аргументами командной строки. */    scan_args(argc, argv, &vars);    if (vars.file_name)    {        vars.file = fopen(vars.file_name, "w");    }    ms_init();    /* Создаем экземпляры фильтров. */    MSFilter  *voidsource=ms_filter_new(MS_VOID_SOURCE_ID);    MSFilter  *dtmfgen=ms_filter_new(MS_DTMF_GEN_ID);    MSSndCard *card_playback=ms_snd_card_manager_get_default_card(ms_snd_card_manager_get());    MSFilter  *snd_card_write=ms_snd_card_create_writer(card_playback);    MSFilter  *voidsink=ms_filter_new(MS_VOID_SINK_ID);    MSDtmfGenCustomTone dtmf_cfg;    /* Устанавливаем имя нашего сигнала, помня о том, что в массиве мы должны     * оставить место для нуля, который обозначает конец строки. */    strncpy(dtmf_cfg.tone_name, "busy", sizeof(dtmf_cfg.tone_name));    dtmf_cfg.duration=1000;    dtmf_cfg.frequencies[0]=440; /* Будем генерировать один тон, частоту второго тона установим в 0.*/    dtmf_cfg.frequencies[1]=0;    dtmf_cfg.amplitude=1.0; /* Такой амплитуде синуса должен соответствовать результат измерения 0.707.*/    dtmf_cfg.interval=0.;    dtmf_cfg.repeat_count=0.;    /* Задаем переменные для хранения результата */    float load=0.;    float latency=0.;    int filter_count=0;    /* Создаем тикер. */    MSTicker *ticker=ms_ticker_new();    ms_ticker_set_priority(ticker, vars.ticker_priority);    /* Соединяем фильтры в цепочку. */    ms_filter_link(voidsource, 0, dtmfgen, 0);    ms_filter_link(dtmfgen, 0, voidsink, 0);    MSFilter* previous_filter=dtmfgen;    int gain=1;    int i;    printf("# filters load\n");    if (vars.file)    {        fprintf(vars.file, "# filters load\n");    }    while ((load <= 99.) && (filter_count < vars.limit))    {        // Временно отключаем  "поглотитель" пакетов от схемы.        ms_filter_unlink(previous_filter, 0, voidsink, 0);        MSFilter  *volume;        for (i=0; i<vars.step; i++)        {            volume=ms_filter_new(MS_VOLUME_ID);            ms_filter_call_method(volume, MS_VOLUME_SET_DB_GAIN, &gain);            ms_filter_link(previous_filter, 0, volume, 0);            previous_filter = volume;        }        // Возвращаем "поглотитель" пакетов в схему.        ms_filter_link(volume, 0, voidsink, 0);        /* Подключаем источник тактов. */        ms_ticker_attach(ticker,voidsource);        /* Включаем звуковой генератор. */        ms_filter_call_method(dtmfgen, MS_DTMF_GEN_PLAY_CUSTOM, (void*)&dtmf_cfg);        /* Даем, время 100 миллисекунд, чтобы были накоплены данные для усреднения. */        ms_usleep(500000);        /* Читаем результат измерения. */        load=ms_ticker_get_average_load(ticker);        filter_count=filter_count + vars.step;        /* Отключаем источник тактов. */        ms_ticker_detach(ticker,voidsource);        printf("%i  %f\n", filter_count, load);        if (vars.file) fprintf(vars.file,"%i  %f\n", filter_count, load);    }    if (vars.file) fclose(vars.file);}

Сохраняем под именем mstest13.c и компилируем командой:


$ gcc mstest13.c -o mstest13 `pkg-config mediastreamer --libs --cflags`

Далее запускаем наш инструмент, чтобы оценить нагрузку тикера работающего с наименьшим приоритетом:


$ ./mstest13 --step 100  --limit 40000 -tprio 0 -o log0.txt

$ ./mstest13 --step 100  --limit 40000 -tprio 1 -o log1.txt

$ ./mstest13 --step 100  --limit 40000 -tprio 2 -o log2.txt

Далее "скармливаем" получившиеся файлы log0.txt, log1.txt, log2.txt великолепной утилите gnuplot:


$ gnuplot -e  "set terminal png; set output 'load.png'; plot 'log0.txt' using 1:2 with lines , 'log1.txt' using 1:2 with lines, 'log2.txt' using 1:2 with lines"

В результате работы программы будет создан файл load.png, в котором будет отрисован график имеющий следующий вид:

По вертикали отложена нагрузка тикера в процентах, по горизонтали количество добавленных фильтров нагрузки.
На этом графике мы видим, что как и ожидалось, для приоритета 2 (голубая линия), первый заметный выброс наблюдается при подключенных 6000 фильтрах, когда как для приоритетов 0 (фиолетовая) и 1(зеленая) выбросы появляются раньше, при 1000 и 3000 фильтров соответственно.


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


Перенос работы в другой тред


Если ваша задача может быть разделена на два и более треда, то тут все просто создаёте один или больше новых тикеров и подключаете на каждый свой граф фильтров. Если задача не может быть распараллелена, то можно её разделить "поперек", разбив цепочку фильтров на несколько сегментов, каждый из которых будет работать в отдельном треде (т.е. со своим тикером). Далее нужно будет выполнить "сшивку" потоков данных, чтобы выходные данные первого сегмента попадали на вход следующего. Такой перенос данных между тредами выполняется с помощью двух специальных фильтров MS_ITC_SINK и MS_ITC_SOURCE, они имеют общее название "интертикеры".


Интертикеры


Фильтр MS_ITC_SINK обеспечивает вывод данных из треда он имеет только один вход, выходов у него нет. Фильтр MS_ITC_SOURCE обеспечивает асинхронный ввод данных в тред, он обладает одним выходом, входов не имеет. В терминах медиастримера, эта пара фильтров дает возможность передавать данные между фильтрами работающими от разных тикеров.


Чтобы началась передача данных, эти фильтры нужно соединить, но не так как мы это делали с обычными фильтрами, т.е. функцией ms_filter_link(). В данном случае, используется метод MS_ITC_SINK_CONNECT фильтра MS_ITC_SINK:


ms_filter_call_method (itc_sink, MS_ITC_SINK_CONNECT, itc_src)

Метод связывает два фильтра с помощью асинхронной очереди. Метода для разъединения интертикеров нет.


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


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


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

Соответствующий код программы показан ниже.


Файл mstest14.c Переменная вычислительная нагрузка c интертикерами
/* Файл mstest14.c Переменная вычислительная нагрузка c интертикерами. */#include <stdio.h>#include <signal.h>#include <mediastreamer2/msfilter.h>#include <mediastreamer2/msticker.h>#include <mediastreamer2/dtmfgen.h>#include <mediastreamer2/mssndcard.h>#include <mediastreamer2/msvolume.h>#include <mediastreamer2/msitc.h>/*----------------------------------------------------------*/struct _app_vars{    int  step;              /* Количество фильтров добавляемых за раз. */    int  limit;             /* Количество фильтров на котором закончить работу. */    int  ticker_priority;   /* Приоритет тикера. */    char* file_name;        /* Имя выходного файла. */    FILE *file;};typedef struct _app_vars app_vars;/*----------------------------------------------------------*//* Функция преобразования аргументов командной строки в  * настройки программы. */void  scan_args(int argc, char *argv[], app_vars *v){    char i;    for (i=0; i<argc; i++)    {        if (!strcmp(argv[i], "--help"))        {            char *p=argv[0]; p=p + 2;            printf("  %s computational load diveded for two threads.\n\n", p);            printf("--help      List of options.\n");            printf("--version   Version of application.\n");            printf("--step      Filters count per step.\n");            printf("--tprio     Ticker priority:\n"                    "            MS_TICKER_PRIO_NORMAL   0\n"                     "            MS_TICKER_PRIO_HIGH     1\n"                    "            MS_TICKER_PRIO_REALTIME 2\n");            printf("--limit     Filters count limit.\n");            printf("-o          Output file name.\n");            exit(0);        }        if (!strcmp(argv[i], "--version"))        {            printf("0.1\n");            exit(0);        }        if (!strcmp(argv[i], "--step"))        {            v->step = atoi(argv[i+1]);            printf("step: %i\n", v->step);        }        if (!strcmp(argv[i], "--tprio"))        {            int prio = atoi(argv[i+1]);            if ((prio >=MS_TICKER_PRIO_NORMAL) && (prio <= MS_TICKER_PRIO_REALTIME))            {                 v->ticker_priority = atoi(argv[i+1]);                printf("ticker priority: %i\n", v->ticker_priority);            }            else            {                printf(" Bad ticker priority: %i\n", prio);                exit(1);            }        }        if (!strcmp(argv[i], "--limit"))        {            v->limit = atoi(argv[i+1]);            printf("limit: %i\n", v->limit);        }        if (!strcmp(argv[i], "-o"))        {            v->file_name=argv[i+1];            printf("file namet: %s\n", v->file_name);        }    }}/*----------------------------------------------------------*//* Структура для хранения настроек программы. */app_vars vars;/*----------------------------------------------------------*/void saveMyData(){    // Закрываем файл.    if (vars.file) fclose(vars.file);    exit(0);}void signalHandler( int signalNumber ){    static pthread_once_t semaphore = PTHREAD_ONCE_INIT;    printf("\nsignal %i received.\n", signalNumber);    pthread_once( & semaphore, saveMyData );}/*----------------------------------------------------------*/int main(int argc, char *argv[]){    /* Устанавливаем настройки по умолчанию. */    app_vars vars={100, 100500, MS_TICKER_PRIO_NORMAL, 0};    // Подключаем обработчик Ctrl-C.    signal( SIGTERM, signalHandler );    signal( SIGINT,  signalHandler );    /* Устанавливаем настройки настройки программы в      * соответствии с аргументами командной строки. */    scan_args(argc, argv, &vars);    if (vars.file_name)    {        vars.file = fopen(vars.file_name, "w");    }    ms_init();    /* Создаем экземпляры фильтров для первого треда. */    MSFilter  *voidsource = ms_filter_new(MS_VOID_SOURCE_ID);    MSFilter  *dtmfgen    = ms_filter_new(MS_DTMF_GEN_ID);    MSFilter  *itc_sink   = ms_filter_new(MS_ITC_SINK_ID);    MSDtmfGenCustomTone dtmf_cfg;    /* Устанавливаем имя нашего сигнала, помня о том, что в массиве мы должны     * оставить место для нуля, который обозначает конец строки. */    strncpy(dtmf_cfg.tone_name, "busy", sizeof(dtmf_cfg.tone_name));    dtmf_cfg.duration=1000;    dtmf_cfg.frequencies[0]=440; /* Будем генерировать один тон, частоту второго тона установим в 0.*/    dtmf_cfg.frequencies[1]=0;    dtmf_cfg.amplitude=1.0; /* Такой амплитуде синуса должен соответствовать результат измерения 0.707.*/    dtmf_cfg.interval=0.;    dtmf_cfg.repeat_count=0.;    /* Задаем переменные для хранения результата */    float load=0.;    float latency=0.;    int filter_count=0;    /* Создаем тикер. */    MSTicker *ticker1=ms_ticker_new();    ms_ticker_set_priority(ticker1, vars.ticker_priority);    /* Соединяем фильтры в цепочку. */    ms_filter_link(voidsource, 0, dtmfgen, 0);    ms_filter_link(dtmfgen, 0, itc_sink , 0);    /* Создаем экземпляры фильтров для второго треда. */    MSTicker *ticker2=ms_ticker_new();    ms_ticker_set_priority(ticker2, vars.ticker_priority);    MSFilter *itc_src   = ms_filter_new(MS_ITC_SOURCE_ID);    MSFilter *voidsink2 = ms_filter_new(MS_VOID_SINK_ID);    ms_filter_call_method (itc_sink, MS_ITC_SINK_CONNECT, itc_src);    ms_filter_link(itc_src, 0, voidsink2, 0);    MSFilter* previous_filter1=dtmfgen;    MSFilter* previous_filter2=itc_src;    int gain=1;    int i;    printf("# filters load\n");    if (vars.file)    {        fprintf(vars.file, "# filters load\n");    }    while ((load <= 99.) && (filter_count < vars.limit))    {        // Временно отключаем  "поглотители" пакетов от схем.        ms_filter_unlink(previous_filter1, 0, itc_sink, 0);        ms_filter_unlink(previous_filter2, 0, voidsink2, 0);        MSFilter  *volume1, *volume2;        // Делим новые фильтры нагрузки между двумя тредами.        int new_filters = vars.step>>1;        for (i=0; i < new_filters; i++)        {            volume1=ms_filter_new(MS_VOLUME_ID);            ms_filter_call_method(volume1, MS_VOLUME_SET_DB_GAIN, &gain);            ms_filter_link(previous_filter1, 0, volume1, 0);            previous_filter1 = volume1;        }        new_filters = vars.step - new_filters;        for (i=0; i < new_filters; i++)        {            volume2=ms_filter_new(MS_VOLUME_ID);            ms_filter_call_method(volume2, MS_VOLUME_SET_DB_GAIN, &gain);            ms_filter_link(previous_filter2, 0, volume2, 0);            previous_filter2 = volume2;        }        // Возвращаем "поглотители" пакетов в схемы.        ms_filter_link(volume1, 0, itc_sink, 0);        ms_filter_link(volume2, 0, voidsink2, 0);        /* Подключаем источник тактов. */        ms_ticker_attach(ticker2, itc_src);        ms_ticker_attach(ticker1, voidsource);        /* Включаем звуковой генератор. */        ms_filter_call_method(dtmfgen, MS_DTMF_GEN_PLAY_CUSTOM, (void*)&dtmf_cfg);        /* Даем, время, чтобы были накоплены данные для усреднения. */        ms_usleep(500000);        /* Читаем результат измерения. */        load=ms_ticker_get_average_load(ticker1);        filter_count=filter_count + vars.step;        /* Отключаем источник тактов. */        ms_ticker_detach(ticker1, voidsource);        printf("%i  %f\n", filter_count, load);        if (vars.file) fprintf(vars.file,"%i  %f\n", filter_count, load);    }    if (vars.file) fclose(vars.file);}

Далее компилируем и запускаем нашу программу с тикерами, работающими с наименьшим приоритетом:


$ ./mstest14 --step 100  --limit 40000 -tprio 0 -o log4.txt

Результат измерений получится следующий:



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


При необходимости можно соединить треды работающие на разных хостах, только при этом вместо интертикеров использовать RTP-сессию (как это мы делали ранее создавая переговорное устройство), здесь также потребуется учесть, что размер RTP-пакетов ограничен сверху величиной MTU.


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


Заключение


Эта статья завершает цикл о Медиастримере2. Вне всякого сомнения, материал статей освещает лишь часть возможностей и особенностей работы этого движка. Цикл нужно рассматривать как первый шаг к освоению этой технологии.

Подробнее..

Из песочницы Самые простые конечные автоматы или стейт-машины в три шага

01.07.2020 18:14:54 | Автор: admin
image

Привет, Хабр!

Перейдем сразу к делу, но небольшая предыстория всё-таки нужна: полтора года назад возникла необходимость реализовать простую стейт-машину (конечный автомат), владея теорией с университета, я был уверен в тривиальности данной задачи (все мы оптимисты).


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


Вскоре я наткнулся на эту статью, которая подтвердила отсутствие удобных решений.


Что сделал тогда?


Так как задачу требовалось решить быстро (ну как обычно), то мой конечный автомат был реализован с помощью словарей, то есть:


  • есть список состояний (Enum)
  • список сигналов (замена классическому входному алфавиту)
  • словарь (map): состояние-сигнал-состояние

Таким образом, в нужном месте функция издает сигнал и, в зависимости от текущего состояния и сигнала, происходит переход (устанавливается следующее состояние)


Но что дальше?


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


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


Спустя год разработки...


Графический редактор


Реализован на wpf с использованием ReactiveUI.


Тут у нас структура конечного автомата в виде графа и в виде таблицы переходов.
Накидываем узлы, соединяем их и сохраняем в xml файл.


image


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


Для тех, кому неохота клацать на ссылку (лень переходить в репозиторий)

Возможности


Две темы


image


Два представления конечного автомата:


  • в виде графа
  • в виде таблицы переходов

Валидация


  • уникальные имена для состояний и переходов
  • отсутствие достижимых состояний (состояний без переходов)

Добавление узлов и соединений


image


Отмена действий


image


Сворачивание и перемещение узлов


image


Масштабирование


image


Выделение элементов


image


Наименования для состояний и переходов


image


Перемещение переходов


image


Удаление переходов


image


Импорт/Экспорт из/в xml



<?xml version="1.0" encoding="utf-8"?><StateMachine>  <States>    <State Name="Start" Position="37, 80" IsCollapse="False" />    <State Name="State 1" Position="471, 195.54" IsCollapse="False" />    <State Name="State 2" Position="276, 83.03999999999999" IsCollapse="False" />  </States>  <StartState Name="Start" />  <Transitions>    <Transition Name="Transition 2" From="State 2" To="State 1" />    <Transition Name="Transition 1" From="Start" To="State 2" />  </Transitions></StateMachine>

Сохранение схемы в PNG/JPEG


image


Библиотека


Реализуем конечный автомат в три шага:


  1. Создаем конечный автомат и инициализируем его структуру сохраненным из редактора файлом.

    StateMachine stateMachine = new StateMachine("scheme.xml");<br>
    
  2. Основную логику описываем в методах, которые затем навешиваем на события, которых предоставляется достаточно.

    stateMachine.GetState("State1").OnExit(Action1);stateMachine.GetState("State2").OnEntry(Action2);stateMachine.GetTransition("Transition1").OnInvoke(Action3);stateMachine.OnChangeState(Action4);
    
  3. Запускаем.

    stateMachine.Start(parameters);
    

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


А что с переходами?


Для перехода внутри функции, которая обрабатывает Entry/Exit в состояние, вызываем:


StateMachine.InvokeTransition("Transition1", parameters);

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


Что есть ещё?


  • Параметры для переходов и состояний.
  • Data словарь с данными, которые хранятся внутри StateMachine и используются для обмена данными между состояниями.

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


Для тех, кому неохота клацать на ссылку (лень переходить в репозиторий)

Возможности:


  • Начальное состояние
  • События входа и выхода для состояния
  • Событие выполнение для перехода
  • Параметры для перехода
  • Параметры для входа/выхода состояния
  • Событие изменения состояния
  • Данные для обмена между состояниями
  • Событие изменения данных
  • Импорт/Экспорт из/в xml
  • Логирование

Будущее проекта


Диплом был защищен на отлично, но забрасывать проект не хотелось бы.
Поэтому буду рад помощи с реализацией нижеописанных пунктов.
Также, если Вы нашли ошибки или у вас есть другие идеи пишите, буду очень рад!


Библиотека. Возможные улучшения:


  1. Асинхронность
  2. Таймеры
  3. Вложенные конечные автоматы
  4. Магия работы с элементами из схемы

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


И работа с ними происходит так:


stateMachine.GetState("State1");

А хотелось бы так


stateMachine.State1;

Сразу скажу, что dynamic не подойдет так как все равно нужно помнить название.
Тут наверное нужна какая-то кодо-генерация, но решение пока что не нашел.


Графический редактор. Возможные улучшения:


  1. Локализация
  2. Шейдеры для отрисовки элементов схемы.
  3. Вложенные конечные автоматы
  4. Автораспределение узлов
    волшебная кнопка автокомпоновки элементов на канвасе
  5. Кроссплатформенность
    Перевод проекта на AvaloniaUI

Выводы


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

Ссылки


Графический редактор, исходники на GitHub: SimpleStateMachineNodeEditor
Библиотека, исходники на GitHub: SimpleStateMachineLibrary

Подробнее..

IDA Pro работа с библиотечным кодом (не WinAPI)

01.07.2020 20:13:32 | Автор: admin

Всем привет,



При работе в IDA мне, да и, наверняка, вам тоже, часто приходится иметь дело с приложениями, которые имеют достаточно большой объём кода, не имеют символьной информации и, к тому же, содержат много библиотечного кода. Зачастую, такой код нужно уметь отличать от написанного пользователем. И, если на вход библиотечного кода подаются только int, void *, да const char *, можно отделаться одними лишь сигнатурами (созданные с помощью FLAIR-утилит sig-файлы). Но, если нужны структуры, аргументы, их количество, тут без дополнительной магии не обойдёшься В качестве примера я буду работать с игрой для Sony Playstation 1, написанной с использованием PSYQ v4.7.


Дополнительная магия


Представим ситуацию: вам попалась прошивка от какой-нибудь железки. Обычный Bare-metal ROM (можно даже с RTOS). Или же ROM игры. В подобных случаях, скорее всего, при компиляции использовался какой-то SDK/DDK, у которого имеется набор LIB/H/OBJ файлов, которые вклеиваются линкером в итоговый файл.


Наш план действий будет примерно таким:


  1. Взять все lib/obj-файлы, и создать из них сигнатуры (или набор сигнатур). Это поможет нам отделить статически влинкованный библиотечный код.
  2. Взять все h-файлы и создать из них библиотеку типов. Эта библиотека хранит не только типы данных, но и информацию об именах и типах аргументов функций, в которых объявленные типы используются.
  3. Применить сигнатуры, чтобы у нас определились библиотечные функции и их имена.
  4. Применить библиотеки типов, чтобы применить прототипы функций и используемые типы данных.

Создаём sig-файлы


Для создания файла сигнатур необходимо воспользоваться набором FLAIR-утилит, доступных лицензионным пользователям IDA. Список необходимых утилит следующий:


  • pcf LIB/OBJ-parser, создаёт PAT-файл из COFF-объектных файлов
  • pelf LIB/OBJ-парсер, создаёт PAT-файл из ELF-файлов (Unix)
  • plb LIB/OBJ-parser, создаёт PAT-файл из OMF-объектных файлов
  • pmacho MACH-O-парсер, создаёт PAT-файл из MACH-O-файлов (MacOS)
  • ppsx OBJ-парсер, создаёт PAT-файл из библиотечных файлов PSYQ
  • ptmobj OBJ-парсер, создаёт PAT-файл из библиотечных файлов Trimedia
  • sigmake конвертирует ранее созданный PAT-файл в SIG-файл, перевариваемый IDA

В моём случае это ppsx. Собираю bat-файл, в котором перечисляю все lib- и obj-файлы, и добавляю к каждой строке вызов ppsx, чтобы получилось формирование итогового PAT-файла. Получилось следующее содержимое:


run_47.bat
@echo offppsx -a 2MBYTE.OBJ psyq47.patppsx -a 8MBYTE.OBJ psyq47.patppsx -a LIBAPI.LIB psyq47.patppsx -a LIBC.LIB psyq47.patppsx -a LIBC2.LIB psyq47.patppsx -a LIBCARD.LIB psyq47.patppsx -a LIBCD.LIB psyq47.patppsx -a LIBCOMB.LIB psyq47.patppsx -a LIBDS.LIB psyq47.patppsx -a LIBETC.LIB psyq47.patppsx -a LIBGPU.LIB psyq47.patppsx -a LIBGS.LIB psyq47.patppsx -a LIBGTE.LIB psyq47.patppsx -a LIBGUN.LIB psyq47.patppsx -a LIBHMD.LIB psyq47.patppsx -a LIBMATH.LIB psyq47.patppsx -a LIBMCRD.LIB psyq47.patppsx -a LIBMCX.LIB psyq47.patppsx -a LIBPAD.LIB psyq47.patppsx -a LIBPRESS.LIB psyq47.patppsx -a LIBSIO.LIB psyq47.patppsx -a ashldi3.obj psyq47.patppsx -a ashrdi3.obj psyq47.patppsx -a CACHE.OBJ psyq47.patppsx -a clear_cache.obj psyq47.patppsx -a CLOSE.OBJ psyq47.patppsx -a cmpdi2.obj psyq47.patppsx -a CREAT.OBJ psyq47.patppsx -a ctors.obj psyq47.patppsx -a divdi3.obj psyq47.patppsx -a dummy.obj psyq47.patppsx -a eh.obj psyq47.patppsx -a eh_compat.obj psyq47.patppsx -a exit.obj psyq47.patppsx -a ffsdi2.obj psyq47.patppsx -a fixdfdi.obj psyq47.patppsx -a fixsfdi.obj psyq47.patppsx -a fixtfdi.obj psyq47.patppsx -a fixunsdfdi.obj psyq47.patppsx -a fixunsdfsi.obj psyq47.patppsx -a fixunssfdi.obj psyq47.patppsx -a fixunssfsi.obj psyq47.patppsx -a fixunstfdi.obj psyq47.patppsx -a fixunsxfdi.obj psyq47.patppsx -a fixunsxfsi.obj psyq47.patppsx -a fixxfdi.obj psyq47.patppsx -a floatdidf.obj psyq47.patppsx -a floatdisf.obj psyq47.patppsx -a floatditf.obj psyq47.patppsx -a floatdixf.obj psyq47.patppsx -a FSINIT.OBJ psyq47.patppsx -a gcc_bcmp.obj psyq47.patppsx -a LSEEK.OBJ psyq47.patppsx -a lshrdi3.obj psyq47.patppsx -a moddi3.obj psyq47.patppsx -a muldi3.obj psyq47.patppsx -a negdi2.obj psyq47.patppsx -a new_handler.obj psyq47.patppsx -a op_delete.obj psyq47.patppsx -a op_new.obj psyq47.patppsx -a op_vdel.obj psyq47.patppsx -a op_vnew.obj psyq47.patppsx -a OPEN.OBJ psyq47.patppsx -a PROFILE.OBJ psyq47.patppsx -a pure.obj psyq47.patppsx -a read.obj psyq47.patppsx -a shtab.obj psyq47.patppsx -a snctors.obj psyq47.patppsx -a SNDEF.OBJ psyq47.patppsx -a SNMAIN.OBJ psyq47.patppsx -a SNREAD.OBJ psyq47.patppsx -a SNWRITE.OBJ psyq47.patppsx -a trampoline.obj psyq47.patppsx -a ucmpdi2.obj psyq47.patppsx -a udiv_w_sdiv.obj psyq47.patppsx -a udivdi3.obj psyq47.patppsx -a udivmoddi4.obj psyq47.patppsx -a umoddi3.obj psyq47.patppsx -a varargs.obj psyq47.patppsx -a write.obj psyq47.patppsx -a LIBSND.LIB psyq47.patppsx -a LIBSPU.LIB psyq47.patppsx -a LIBTAP.LIB psyq47.patppsx -a LOW.OBJ psyq47.patppsx -a MCGUI.OBJ psyq47.patppsx -a MCGUI_E.OBJ psyq47.patppsx -a NOHEAP.OBJ psyq47.patppsx -a NONE3.OBJ psyq47.patppsx -a NOPRINT.OBJ psyq47.patppsx -a POWERON.OBJ psyq47.pat

LIBSN.LIB файл имеет формат, отличный от остальных библиотек, поэтому пришлось разложить его на OBJ-файлы утилитой PSYLIB2.EXE, которая входит в комплект PSYQ. Запускаем run_47.bat. Получаем следующий выхлоп:


run_47.bat output
2MBYTE.OBJ: skipped 0, total 18MBYTE.OBJ: skipped 0, total 1LIBAPI.LIB: skipped 0, total 89LIBC.LIB: skipped 0, total 55LIBC2.LIB: skipped 0, total 50LIBCARD.LIB: skipped 0, total 18LIBCD.LIB: skipped 0, total 51LIBCOMB.LIB: skipped 0, total 3LIBDS.LIB: skipped 0, total 36LIBETC.LIB: skipped 0, total 8LIBGPU.LIB: skipped 0, total 60LIBGS.LIB: skipped 0, total 167LIBGTE.LIB: skipped 0, total 535LIBGUN.LIB: skipped 0, total 2LIBHMD.LIB: skipped 0, total 585LIBMATH.LIB: skipped 0, total 59LIBMCRD.LIB: skipped 0, total 7LIBMCX.LIB: skipped 0, total 31LIBPAD.LIB: skipped 0, total 21LIBPRESS.LIB: skipped 0, total 7LIBSIO.LIB: skipped 0, total 4ashldi3.obj: skipped 0, total 1ashrdi3.obj: skipped 0, total 1CACHE.OBJ: skipped 0, total 1clear_cache.obj: skipped 0, total 1CLOSE.OBJ: skipped 0, total 1cmpdi2.obj: skipped 0, total 1CREAT.OBJ: skipped 0, total 1ctors.obj: skipped 0, total 0divdi3.obj: skipped 0, total 1dummy.obj: skipped 0, total 1Fatal: Illegal relocation information at file pos 0000022Deh_compat.obj: skipped 0, total 1exit.obj: skipped 0, total 1ffsdi2.obj: skipped 0, total 1fixdfdi.obj: skipped 0, total 1fixsfdi.obj: skipped 0, total 1fixtfdi.obj: skipped 0, total 0fixunsdfdi.obj: skipped 0, total 1fixunsdfsi.obj: skipped 0, total 1fixunssfdi.obj: skipped 0, total 1fixunssfsi.obj: skipped 0, total 1fixunstfdi.obj: skipped 0, total 0fixunsxfdi.obj: skipped 0, total 0fixunsxfsi.obj: skipped 0, total 0fixxfdi.obj: skipped 0, total 0floatdidf.obj: skipped 0, total 1floatdisf.obj: skipped 0, total 1floatditf.obj: skipped 0, total 0floatdixf.obj: skipped 0, total 0FSINIT.OBJ: skipped 0, total 1gcc_bcmp.obj: skipped 0, total 1LSEEK.OBJ: skipped 0, total 1lshrdi3.obj: skipped 0, total 1moddi3.obj: skipped 0, total 1muldi3.obj: skipped 0, total 1negdi2.obj: skipped 0, total 1Fatal: Illegal relocation information at file pos 0000013Dop_delete.obj: skipped 0, total 1op_new.obj: skipped 0, total 1op_vdel.obj: skipped 0, total 1op_vnew.obj: skipped 0, total 1OPEN.OBJ: skipped 0, total 1PROFILE.OBJ: skipped 0, total 1pure.obj: skipped 0, total 1Fatal: Unknown record type 60 at 0000015Fshtab.obj: skipped 0, total 0Fatal: Unknown record type 60 at 000000EESNDEF.OBJ: skipped 0, total 0SNMAIN.OBJ: skipped 0, total 1SNREAD.OBJ: skipped 0, total 1SNWRITE.OBJ: skipped 0, total 1trampoline.obj: skipped 0, total 0ucmpdi2.obj: skipped 0, total 1udiv_w_sdiv.obj: skipped 0, total 1udivdi3.obj: skipped 0, total 1udivmoddi4.obj: skipped 0, total 1umoddi3.obj: skipped 0, total 1varargs.obj: skipped 0, total 1Fatal: Unknown record type 60 at 00000160LIBSND.LIB: skipped 0, total 223LIBSPU.LIB: skipped 0, total 126LIBTAP.LIB: skipped 0, total 1LOW.OBJ: skipped 0, total 1Fatal: can't find symbol F003MCGUI_E.OBJ: skipped 0, total 1NOHEAP.OBJ: skipped 0, total 1NONE3.OBJ: skipped 0, total 1NOPRINT.OBJ: skipped 0, total 1POWERON.OBJ: skipped 0, total 1

Видим некоторое количество ошибок парсинга, но, в тех файлах всего 1 сигнатура (total 1), поэтому, думаю, что это не критично. Далее преобразовываем PAT-файл в SIG-файл:


sigmake -n"PsyQ v4.7" psyq47.pat psyq47.sigpsyq47.sig: modules/leaves: 1345/2177, COLLISIONS: 21See the documentation to learn how to resolve collisions.

В итоге получаем следующий список файлов:


  • psyq47.err его не трогаем
  • psyq47.exc его нужно будет отредактировать
  • psyq47.pat его тоже не трогаем

Открываем на редактирование .exc-файл. Видим:


;--------- (delete these lines to allow sigmake to read this file); add '+' at the start of a line to select a module; add '-' if you are not sure about the selection; do nothing if you want to exclude all modules

Если удалить ---------, всё, что содержится в файле ниже, будет учитываться. Давайте взглянем на то, что там есть. Вот пример:


CdPosToInt                                          60 A21C 0000839001008690022903008010050021104500401002000F00633021104300DsPosToInt                                          60 A21C 0000839001008690022903008010050021104500401002000F00633021104300

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


В итоге, если всё сделано правильно, получаем SIG-файл. Его нужно положить в соответствующую папку в каталоге установка IDA.


Создаём til-файлы


Эти файлы нужны для хранения информации о типах, об аргументах функций и т.п. По-умолчанию. Создаются они с помощью утилиты tilib, которую необходимо положить в каталог с Идой (ей, почему-то, нужен ida.hlp файл).


Данной утилите нужно скормить include-файлы вашего SDK/DDK. При том парсинг этой утилитой отличается от такового средством "Parse C header file..." в самой IDA. Вот описание из readme:


Its functionality overlaps with "Parse C header file..." from IDA Pro.
However, this utility is easier to use and provides more control
over the output. Also, it can handle the preprocessor symbols, while
the built-in command ignores them.

У этой утилиты есть один нюанс: она по умолчанию использует режим, когда символы заманглены, либо имеют стоящее в начале имени нижнее подчёркивание. В случае работы со статически влинкованным кодом этот режим нужно выключить флагом -Gn.


По-умолчанию, данная утилита принимает на вход только один include-файл. Если же файлов много, нужно соорудить include-файл следующего содержания:


#include "header1.h"#include "header2.h"#include "header3.h"// ...

Этот файл передаётся с помощью флага -hFileName.h. Далее, передаём путь поиска остальных header-файлов и получаем следующую командую строку:


tilib -c -Gn -I. -hpsyq47.h psyq47.til

На выходе получаем til-файл, пригодный для использования. Кладём его в соответствующий каталог IDA: sig\mips.


Проверяем результат


Закидываем ROM-файл в IDA, дожидаемся окончания анализа. Далее, необходимо указать компилятор. Для этого заходим в Options->Compiler:



Теперь просто меняем Unknown на GNU C++ (в случае PSX). Остальное оставляем как есть:



Теперь жмём Shift+F5 (либо меню View->Open subviews->Signatures), жмём Insert и выбираем нужный файл сигнатур:



Жмём OK и ждём, пока применяются сигнатуры (у меня получилось 482 распознанных функции).



Далее необходимо применить библиотеку типов (til-файл). Для этого жмём Shift+F11 (либо View->Open subviews->Type libraries) и понимаем, что IDA не может определить компилятор (не смотря на то, что мы его уже указали):



Но это нам всё равно не помешает выбрать til-файл (всё так же, через Insert):



Получаем то, что так хотели:



Теперь декомпилятор успешно подхватывает информацию о типах, и выхлоп становится куда лучше:



P.S.


Надеюсь, эта информация окажется для вас полезной. Удачного реверс-инжиниринга!

Подробнее..

PVS-Studio теперь в Compiler Explorer

06.07.2020 16:22:04 | Автор: admin
image1.png

Совсем недавно произошло знаменательное событие: PVS-Studio появился в Compiler Explorer! Теперь вы можете быстро и легко проанализировать код на наличие ошибок прямо на сайте godbolt.org (Compiler Explorer). Это нововведение открывает большое количество новых возможностей от утоления любопытства по поводу способностей анализатора до возможности быстро поделиться результатом проверки с другом. О том, как использовать эти возможности, и пойдёт речь в этой статье. Осторожно большие гифки!

Если вам хочется сразу перейти к делу и проанализировать код здесь и сейчас, то вот вам полностью готовая к использованию ссылка. Рекомендую добавить её себе в закладки еще пригодится ;)

Теперь давайте обо всём по порядку. Compiler Explorer интерактивный онлайн-сервис для исследования компиляторов. Здесь вы можете прямо на сайте писать код и сразу видеть, какой ассемблерный вывод сгенерирует для него тот или иной компилятор:

image2.gif

Помимо этого, на сайте существует еще целый набор возможностей, и сегодня мы рассмотрим возможность проведения анализа с помощью PVS-Studio. Для того, чтобы провести анализ, необходимо сделать следующее:

  1. Зайти на сайт godbolt.org,
  2. Во вкладке с выводом компилятора нажать "Add tool...",
  3. В выпадающем списке выбрать "PVS-Studio".

Пример такой последовательности действий вы можете увидеть на анимации ниже:

image3.gif

Готово! Теперь в появившемся окне будут автоматически выводиться все предупреждения, которые выдаются анализатором на ваш код. Анализировать можно как собственноручно написанный код, так и исходные файлы, загруженные по отдельности. Естественно, файлы должны быть самодостаточными и не использовать include на файлы, не входящие в стандартную библиотеку. В противном случае файл не получится ни скомпилировать, ни проверить.

На данный момент анализ с помощью PVS-Studio доступен на сайте для всех версий GCC и Clang под платформы x86 и x64. Мы планируем расширить возможности сайта и на другие поддерживаемые нами компиляторы (например, MSVC или компиляторы для ARM), если на это будет спрос.

Сейчас на сайте включены только General-диагностики уровней error, warning и note. Мы специально не стали включать остальные режимы (Optimization, 64-bit, Custom и MISRA), чтобы в выводе остались только самые важные предупреждения. Также, в отличие от самого PVS-Studio, Compiler Explorer пока не поддерживает языки C# и Java мы планируем запустить анализ кода на этих языках, как только они там появятся :)

Compiler Explorer имеет весьма умную систему окон, поэтому вы можете двигать их или, например, накладывать друг на друга. Если сейчас вас не интересует вывод компилятора, его можно "спрятать". Вот так:

image4.gif

Вы можете как сразу писать код в окне Compiler Explorer, так и загружать отдельные файлы. Для этого нужно нажать "Save/Load" и в открывшейся вкладке выбрать "File system". Также можно "скачать" написанный вами код на компьютер, нажав Ctrl + S.

image5.gif

На сайте нельзя анализировать проекты целиком можно только загружать файлы по отдельности.

Если вам хочется увидеть вывод вашей программы, то можно открыть окно выполнения, нажав "Add new > Execution only" в окне для написания кода (не в окне с компилятором). На гифке ниже вы можете увидеть вывод лабораторной работы, взятой из нашей страницы про бесплатное использование PVS-Studio студентами и преподавателями.

image6.gif

Кстати, вы заметили, что при переходе по ссылкам на godbolt у вас открывается уже заранее вписанный код в заранее расставленных окнах? Да, вы можете генерировать постоянные ссылки, полностью сохраняющие состояние страницы в момент генерации! Для этого вам нужно нажать на кнопку "Share" в правом верхнем углу экрана.

image7.gif

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

Мы планируем использовать эту возможность для работы с клиентами в техподдержке: зачастую использование Compiler Explorer является очень удобным для составления минимально воспроизводимых примеров, которые можно быстро взять и отправить по почте.

Также в выпадающей вкладке "Share" есть пункт создания Embedded-ссылки, с помощью которой можно встроить окно с Compiler Explorer на какой-нибудь другой сайт.

В Compiler Explorer всегда находится актуальная версия PVS-Studio, поэтому после каждого нашего релиза на сайте можно будет найти всё больше и больше ошибок. Тем не менее, использование PVS-Studio на godbolt.org не дает полного представления о его возможностях, ведь PVS-Studio это не только диагностики, но и развитая инфраструктура:

  • Анализ кода на языках C, C++, C# и Java для куда большего количества платформ и компиляторов;
  • Плагины для Visual Studio 2010-2019, JetBrains Rider, IntelliJ IDEA;
  • Возможность интеграции в TeamCity, PlatformIO, Azure DevOps, Travis CI, CircleCI, GitLab CI/CD, Jenkins, SonarQube и т.д.
  • Утилита мониторинга компиляции для проведения анализа независимо от IDE или сборочной системы;
  • И многое, многое другое.

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

Чтобы всегда быть в курсе, следите за нашими новостями. Также читайте наш блог: там мы публикуем не только новости и статьи о поиске багов в реальных проектах, но и различные интересные моменты, связанные с C, C++, C# и Java.

Наши соцсети:



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: George Gribkov. PVS-Studio is now in Compiler Explorer!.
Подробнее..

Применение CQRS amp Event Sourcing в создании платформы для проведения онлайн-аукционов

07.07.2020 14:13:48 | Автор: admin
Коллеги, добрый день! Меня зовут Миша, я работаю программистом.

В настоящей статье я хочу рассказать о том, как наша команда решила применить подход CQRS & Event Sourcing в проекте, представляющем собой площадку для проведения онлайн-аукционов. А также о том, что из этого получилось, какие из нашего опыта можно сделать выводы и на какие грабли важно не наступить тем, кто отправится путем CQRS & ES.
image


Прелюдия


Для начала немного истории и бизнесового бэкграунда. К нам пришел заказчик с платформой для проведения так называемых timed-аукционов, которая была уже в продакшене и по которой было собрано некоторое количество фидбэка. Заказчик хотел, чтоб мы сделали ему платформу для live-аукционов.

Теперь чуть-чуть терминологии. Аукцион это когда продаются некие предметы лоты (lots), а покупатели (bidders) делают ставки (bids). Обладателем лота становится покупатель, предложивший самую большую ставку. Timed-аукцион это когда у каждого лота заранее определен момент его закрытия. Покупатели делают ставки, в какой-то момент лот закрывается. Похоже на ebay.

Timed-платформа была сделана классически, с применением CRUD. Лоты закрывало отдельное приложение, запускаясь по расписанию. Работало все это не слишком надежно: какие-то ставки терялись, какие-то делались как будто бы от лица не того покупателя, лоты не закрывались или закрывались по несколько раз.

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

Обе платформы в принципе работают в реальном времени, но если в случае timed все покупатели находятся в равном положении, то в случае live крайне важно, чтоб онлайн-покупатели могли успешно соревноваться с находящимися в комнате. То есть система должна быть очень быстрой и надежной. Печальный опыт timed-платформы недвусмысленно говорил нам, что классический CRUD нам не подходит.

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

Какая еще есть специфика работы онлайн-аукционов:

  • Много пользователей одновременно пытаются воздействовать на один и тот же объект в системе текущий лот. Покупатели делают свои ставки, клерк вводит в систему ставки из комнаты, закрывает лот, открывает следующий. В каждый момент времени в системе можно сделать ставку только одной величины например, 5 рублей. И только один пользователь сможет сделать эту ставку.
  • Нужно хранить всю историю действий над объектами системы, чтобы в случае необходимости можно было посмотреть, кто какую ставку сделал.
  • Время отклика системы должно быть очень маленьким ход онлайн-версии аукциона не должен отставать от офлайн, пользователям должно быть понятно, к чему привели их попытки сделать ставку успешны они или нет.
  • Пользователи должны оперативно узнавать обо всех изменениях в ходе аукциона, а не только о результатах своих действий.
  • Решение должно быть масштабируемым несколько аукционов могут проходить одновременно.

Краткий обзор подхода CQRS & ES


Не буду подробно останавливаться на рассмотрении подхода CQRS & ES, материалы об этом есть в интернете и в частности на Хабре (например, вот: Введение в CQRS + Event Sourcing). Однако кратко все же напомню основные моменты:

  • Самое главное в event sourcing: система хранит не данные, а историю их изменения, то есть события. Текущее состояние системы получается последовательным применением событий.
  • Доменная модель делится на сущности, называемые агрегатами. Агрегат имеет версию. События применяются к агрегатам. Применение события к агрегату инкрементирует его версию.
  • События хранятся в write-базе. В одной и той же таблице хранятся события всех агрегатов системы в том порядке, в котором они произошли.
  • Изменения в системе инициируются командами. Команда применяется к одному агрегату. Команда применяется к последней, то есть текущей, версии агрегата. Агрегат для этого выстраивается последовательным применением всех своих событий. Этот процесс называется регидратацией.
  • Для того, чтобы не регидрировать каждый раз с самого начала, какие-то версии агрегата (обычно каждая N-я версия) можно хранить в системе в готовом виде. Такие снимки агрегата называются снапшотами. Тогда для получения агрегата последней версии при регидратации к самому свежему снапшоту агрегата применяются события, случившиеся после его создания.
  • Команда обрабатывается бизнес-логикой системы, в результате чего получается, в общем случае, несколько событий, которые сохраняются в write-базу.
  • Кроме write-базы, в системе может еще быть read-база, которая хранит данные в форме, в которой их удобно получать клиентам системы. Сущности read-базы не обязаны соответствовать один к одному агрегатам системы. Read-база обновляется обработчиками событий.
  • Таким образом, у нас получается разделение команд и запросов к системе Command Query Responsibility Segregation (CQRS): команды, изменяющие состояние системы, обрабатываются write-частью; запросы, не изменяющие состояние, обращаются к read-части.



Реализация. Тонкости и сложности.


Выбор фреймворка


В целях экономии времени, а также в силу отсутствия специфического опыта мы решили, что нужно использовать какой-то фреймворк для CQRS & ES.

В целом наш технологический стек это Microsoft, то есть .NET и C#. База данных Microsoft SQL Server. Хостится все в Azure. На этом стеке была сделана timed-платформа, логично было и live-платформу делать на нем.

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

Зачем вообще нужен фреймворк CQRS & ES? Он может из коробки решать такие задачи и поддерживать такие аспекты реализации как:

  • Сущности агрегата, команды, события, версионирование агрегатов, регидратация, механизм снапшотов.
  • Интерфейсы для работы с разными СУБД. Сохранение/загрузка событий и снапшотов агрегатов в/из write-базы (event store).
  • Интерфейсы для работы с очередями отправка в соответствующие очереди команд и событий, чтение команд и событий из очереди.
  • Интерфейс для работы с веб-сокетами.

Таким образом, с учетом использования Chinchilla, к нашему стеку добавились:

  • Azure Service Bus в качестве шины команд и событий, Chinchilla поддерживает его из коробки;
  • Write- и read-базы Microsoft SQL Server, то есть обе они SQL-базы. Не скажу, что это является результатом осознанного выбора, скорее по историческим причинам.

Да, фронтенд сделан на Angular.

Как я уже говорил, одно из требований к системе чтобы пользователи максимально быстро узнавали о результатах своих действий и действий других пользователей это относится и к покупателям, и к клерку. Поэтому мы используем SignalR и веб-сокеты для оперативного обновления данных на фронтенде. Chinchilla поддерживает интеграцию с SignalR.

Выбор агрегатов


Одной из первых вещей, которую надо сделать при реализации подхода CQRS & ES это определить, как доменная модель будет делиться на агрегаты.

В нашем случае доменная модель состоит из нескольких основных сущностей, примерно таких:

public class Auction{     public AuctionState State { get; private set; }     public Guid? CurrentLotId { get; private set; }     public List<Guid> Lots { get; }}public class Lot{     public Guid? AuctionId { get; private set; }     public LotState State { get; private set; }     public decimal NextBid { get; private set; }     public Stack<Bid> Bids { get; }} public class Bid{     public decimal Amount { get; set; }     public Guid? BidderId { get; set; }}


У нас получилось два агрегата: Auction и Lot (с Bidами). В общем, логично, но мы не учли одного того, что при таком делении состояние системы у нас размазалось по двум агрегатам, и в ряде случаев для сохранения консистентности мы должны вносить изменения в оба агрегата, а не в один. Например, аукцион можно поставить на паузу. Если аукцион на паузе, то нельзя делать ставки на лот. Можно было бы ставить на паузу сам лот, но аукциону на паузе тоже нельзя обрабатывать никаких команд, кроме как снять с паузы.

В качестве альтернативного варианта можно было сделать только один агрегат, Auction, со всеми лотами и ставками внутри. Но такой объект будет довольно тяжелым, потому что лотов в аукционе может быть до нескольких тысяч и ставок на один лот может быть несколько десятков. За время жизни аукциона у такого агрегата будет очень много версий, и регидратация такого агрегата (последовательное применение к агрегату всех событий), если не делать снапшотов агрегатов, будет занимать довольно продолжительное время. Что для нашей ситуации неприемлемо. Если же использовать снапшоты (мы их используем), то сами снапшоты будут весить очень много.

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

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

Мы на данном этапе эволюции проекта живем с двумя агрегатами, Auction и Lot, и нарушаем архитектуру, меняя в рамках некоторых команд оба агрегата.

Применение команды к определенной версии агрегата


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

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

Мы используем второй вариант. Так у команд больше шансов выполниться. Потому что в той части приложения, которая отправляет команды (в нашем случае это фронтенд), текущая версия агрегата с некоторой вероятностью будет отставать от реальной версии на бэкенде. Особенно в условиях, когда команд отправляется много, и верcия агрегата меняется часто.

Ошибки при выполнении команды с использованием очереди


В нашей реализации, в большой степени обусловленной использованием Chinchilla, обработчик команд читает команды из очереди (Microsoft Azure Service Bus). Мы у себя явно разделяем ситуации, когда команда зафейлилась по техническим причинам (таймауты, ошибки подключения к очереди/базе) и когда по бизнесовым (попытка сделать на лот ставку той же величины, что уже была принята, и проч.). В первом случае попытка выполнить команду повторяется, пока не выйдет заданное в настройках очереди число повторений, после чего команда отправляется в Dead Letter Queue (отдельный топик для необработанных сообщений в Azure Service Bus). В случае бизнесового эксепшена команда отправляется в Dead Letter Queue сразу.



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


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

Однако, в отличие от ситуации с невыполненной командой, здесь все хуже может получиться так, что команда выполнилась и события в write-базу записались, но обработка их обработчиками зафейлилась. И если один из этих обработчиков обновляет read-базу, то read-база не обновится. То есть окажется в неконсистентном состоянии. Благодаря наличию механизма повторных попыток обработки события read-база почти всегда, в конечном итоге, обновляется, однако вероятность того, что после всех попыток она останется поломанной, все-таки остается.



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

В итоге, в качестве временной меры мы отказались от использования Azure Service Bus для передачи событий из write-части приложения в read-часть. Вместо нее используется так называемая In-Memory Bus, что позволяет обрабатывать команду и события в одной транзакции и в случае неудачи откатить все целиком.



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

Отправка команды в качестве реакции на событие


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

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


В общем случае в результате выполнения одной команды получается несколько событий. Бывает, что на каждое из событий нам нужно сделать какое-то изменение в read-базе. Бывает также, что последовательность событий тоже важна, и в неправильной последовательности обработка событий не отработает как надо. Все это означает, что мы не можем читать из очереди и обрабатывать события одной команды независимо, например, разными экземплярами кода, который читает сообщения из очереди. Плюс к этому нам нужна гарантия того, что события из очереди будут прочитаны в той же последовательности, в которой они были туда отправлены. Либо нам нужно быть готовыми к тому, что не все события команды будут успешно обработаны с первой попытки.



Обработка одного события несколькими обработчиками


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

Например, обработчик события в read-базе добавляет ставку на лот величиной 5 рублей. Первая попытка сделать это будет успешной, а вторую не даст выполнить constraint в базе.



Выводы/Заключение


Сейчас наш проект находится в стадии, когда, как нам кажется, мы наступили уже на бОльшую часть существующих граблей, актуальных для нашей бизнес-специфики. В целом мы считаем свой опыт довольно успешным, CQRS & ES хорошо подходит для нашей предметной области. Дальнейшее развитие проекта видится в отказе от Chinchilla в пользу другого фреймворка, дающего больше гибкости. Впрочем, возможен и вариант отказа от использования фреймворка вообще. Также вероятно будут какие-то изменения в направлении поиска баланса между надежностью с одной стороны и быстротой и масштабируемостью решения с другой.

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

Хочется надеяться, что наш опыт окажется для кого-то полезным, поможет сэкономить время и избежать граблей. Спасибо за внимание.
Подробнее..

IDA Pro каким не должен быть SDK

05.07.2020 22:23:42 | Автор: admin

Приветствую,



Эта статья будет о том, как не нужно делать, когда разрабатываешь SDK для своего продукта. А примером, можно даже сказать, самым ярким, будет IDA Pro. Те, кто хоть раз что-то разрабатывал под неё и старался поддерживать, при чтении этих строк, наверняка, сейчас вздрогнули и покрылись холодным потом. Здесь я собрал опыт сопровождения проектов, начиная с IDA v6.5, и заканчивая последней на момент написания статьи версии v7.5. В общем, погнали.


Краткое описание


SDK для IDA Pro позволяет вам разрабатывать следующие типы приложений:


  • Загрузчики различных форматов
  • Процессорные модули
  • Плагины, расширяющие функционал (процессорных модулей, интерфейса и т.п.)
  • IDC-скрипты (свой внутренний язык) и Python-скрипты (вторые использует стороннюю разработку IDAPython, которая стала неотъемлемой частью IDA)

По информации с сайта Hex-Rays, стоимость плана поддержки SDK 10000 USD. На практике же если у вас есть лицензия, вам даётся код доступа к Support-зоне, в которой вы его скачиваете и работаете с ним. Стоимость же указана на тот случай, если у вас будут появляться вопросы и вы захотите задать их разработчикам: без плана поддержки вам скажут, мол, напишите это сами; с поддержкой же, как я понимаю, отказать вам не могут. К сожалению, я не знаю ни одного человека (фирмы), который купил данный план.


Немного подробнее


С того момента, как у вас появляется желание написать что-то под IDA, и вы скачиваете SDK, вы ступаете на достаточно скользкую дорожку, на которой, к тому же, очень легко сойти с ума. И вот почему:


1) В современной разработке вы, скорее всего, уже привыкли к тому, что у любого более-менее серьёзного проекта есть либо doxygen-сгенерированная справка по API, либо вручную написанная хорошая справка, которая вам говорит что куда передаётся, что нужно заполнять, чтобы работало, и т.п., с кучей примеров.
В IDA практически в большинстве доступных пользователю API указаны лишь имена параметров и их типы, а что туда передавать можно отгадывать лишь по именам аргументов. В последнее время, конечно, со справкой у IDA стало получше, но не то что бы значительно.


2) Во многих заполняемых структурах требуется задавать callback-функции, при этом некоторые указаны как необязательные, мол, не укажешь (передашь NULL) и ладно. В действительности крэши приложения при попытке запуска вашего плагина. И, т.к. колбэков много (пример плагин-отладчик), ты начинаешь поочерёдно задавать все, которые "можно не задавать". В итоге это очень сильно утомляет, ты открываешь x64dbg/ollyDbg, в нём idaq.exe/ida.exe, грузишь плагин, ставишь точки остановки, и пытаешься словить момент, когда управление передаётся в 0x00000000.


Эх, помню те времена, когда так много папок с проектами были забиты 200MB dmp-файлами, которые создавались при крэше IDA Но мне они ничем не помогали.


3) Самая болезненная тема для IDA Pro обратная совместимость. В принципе, это достаточно тяжёлая для любого разработчика задача наперёд продумывать интерфейсы, структуры, модульность и т.п. Поэтому здесь и возникает два пути:


  • Хранить обратную совместимость со всеми старыми версиями
  • Не заниматься обратной совместимостью

В первом случае очень быстро накапливается legacy-код, от которого потом становится невозможно избавиться. Вычистить его редко кому удаётся. Потому как, фактически, нужно бросать все силы не на разработку нового функционала, а на выпиливание/перенос старого.


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


Что же в случае Hex-Rays? Вы удивитесь, но они пошли двумя путями одновременно! Известно, что проект развивается с очень и очень бородатых времён, когда основной целевой платформой был лишь MS-DOS (и, следовательно, реверс-инжиниринг написанных под него приложений). Нужно было поддерживать сегментные регистры, селекторы, параграфы и другую подобную атрибутику. Шло время, в IDA начали появляться другие платформы, процессорные модули и загрузчики, где модель памяти уже была плоской (flat), но пережиток в виде перечисленных мной MS-DOS "фич" сохраняется до сих пор! Весь интерфейс IDA пронизан этим. При разработке процессорных модулей, в который только flat, вам всё равно придётся указываться сегментные регистры (правда уже виртуальные).


А вот с SDK ни о какой обратной совместимости речи идти не может вовсе. В каждой новой версии (даже внутри минорных билдов 6.x и 7.x) что-то да ломается: у колбэков появляются новые аргументы, у старых структур переименовываются и теряются поля, функции API, которые раньше делали одну задачу, теперь делают другую. И таких примеров много.


И ладно бы это всё хоть как-то сопровождалось разработчиком, мол, в этой версии поменялось то и это, теперь нужно так и так. Так нет же! Гайд есть, да: IDA 7.0 SDK: Porting from IDA 4.9-6.x API to IDA 7.0 API, но это всё. Более того, по нему вам не удастся перевести свой проект на новую версию, т.к. он не включает очень многих, но мелких, изменений, о которых, конечно же, вам никто не сообщит. К тому же, это последний гайд для C/C++ разработчика, а с тех пор вышло ещё где-то 5-6 версий SDK.


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


Реальный пример


Когда-то я взял на себя смелость попытаться разработать свой первый плагин-отладчик Motorola 68000 под IDA. В поставляемом SDK был пример отладчика (который, фактически, используется в IDA Pro и сейчас в качестве локального и удалённого), но он был выполнен настолько плохо, что пытаться по нему сделать свой было невозможно. Тогда я полез в интернет и нашёл единственный плагин-отладчик для PS3, который, что забавно, был выполнен на базе того самого кода из SDK.


Чтобы вы понимали, почему код из SDK я так и не решился использовать, вот вам скриншот участка кода, на котором я уже тогда понял, что будет несладко:



Видите cpp-файлы, которые включены через #include? И так по всему исходнику. Тем не менее, тщательно изучив исходный код отладчика PS3, мне удалось вычленить из него что-то рабочее и сделать свой для Sega Mega Drive.


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



Для этого пришлось снова отлаживать IDA, процессорный модуль m68k, и делать исправления для последнего (об этом я писал в "Модернизация IDA Pro. Исправляем косяки процессорных модулей").


Несмотря на все трудности, мне удалось написать хороший отладчик! К сожалению, вышла новая версия SDK В ней изменилась структура debugger_t, отвечающая за отладчик и его колбэки, и всё, что я пытался сделать, приводило к крэшам самой IDA. Спустя время я и с этим справился.


Но вышла новая версия SDK x64, без совместимости с x86! А эмулятор Gens, на базе которого я делал отладчик, не умел в x64, и проект заглох на много лет. Когда же я нашёл эмулятор, который был способен работать в x64, вышло так много версий SDK, что снова пытаться понять, почему мой плагин не работает, я не решился.


Выводы


Проблемы SDK для IDA Pro в отсутствии нормальной документации об изменениях в каждой из версий; в том, что изменения выходят скопом их много, они кардинальные, и за ними очень тяжело угнаться.


Если уж ваша фирма/команда выкладывает какой-то публичный SDK, и у него есть пользователи, которые платят деньги будьте добры, думайте и о них тоже! Их разработки могут помочь вашему продукту стать лучше и популярнее, как это произошло с IDAPython. Понятно, что хранить обратную совместимость очень сложно, но, если уж решились не поддерживать старые версии, постарайтесь документировать все изменения, которые вы делаете.


Я видел на Github большое количество полезных проектов, которые так и остались непортированными на IDA v7.x. Можно подумать, что их функционал стал ненужным в новых версиях? Может и так, но, как по мне, это усталость и нежелание бороться с постоянно меняющимся API в совокупности с хоббийностью проекта.


IDA Pro Book


Ещё хотелось бы вспомнить об одной бесценной книге, которая мне когда-то очень помогла, но которая сейчас абсолютно бесполезна для разработчика плагинов к IDA IDA Pro Book от Chris Eagle. Всё описанное в ней относится к версии 6.x (ориентировочно v6.5-v6.8). С тех пор изменилось практически всё.


Спасибо.

Подробнее..

Играем в Бога, причиняем непрошеную помощь науке и немножечко Сингулярности

29.06.2020 18:10:05 | Автор: admin
Если вы хотя бы на пол шишечки интересуетесь современной наукой, то знаете кто такой Марков. Лауреат премии Просветитель, реально просветитель, автор кучи прекрасных книг, и афигенных роликов на ютубе, а ещё основной двигатель сайта elementy.ru, отличающегося тем, что статьи готовят профессионалы, а не журналисты, со ссылками на первоисточники и никогда никакого хайпа, что очень хорошо для мозговой гигиены. В общем он не только изучает увеличение мозга хомосапиенсов, но и реально его наполняет всяким интересным.



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

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

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

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

Часть первая, про Оптимизацию.


Make it work, Make it right, Make it fast! Kent Beck
Автором симулятора был сын нашего учителя Михаил JellicleFencer Марков. В комментариях под статьей он выложил ссылку на репозиторий: jelliclefencer.visualstudio.com/_git/TribeSim. Написать то он её написал, причём так чтобы происходящее внутри было понятно любому биологу без комментариев. Но времени на то чтобы заниматься оптимизацией у него явно не было. В результате залезши внутрь я в порядке хобби ускорил работу симуляции в 10-20 раз даже не прибегая к unsafe коду и прочим излишествам и потрогав пока только половину тормозящих мест. Читаемость снизилась, надеюсь, не критично. Азур меня почему-то не любит, поэтому я форкнулся на гитхаб вот сюда: github.com/kraidiky/TribeSim и там можно по коммитам проследить как я отъедал в куче мест по 5-15% прироста.

Я планирую ещё какое-то время продолжить этим заниматься и приглашаю желающих поучаствовать в этом занятии. Для этого надо форкнуть репозиторий, сделать коду хорошо, после чего отправить Pull Request. Честно то говоря никогда раньше так не делал за больше чем 20 лет программистской карьеры. Так что если я что-то делаю неправильно поправляйте.

Только давайте с развёрнутыми комментариями, без unsafe и экстримизма. Представьте, что читать и использовать этот код предстоит универовскому профессору, у которого в руках ваша зачётка. :)

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

Есть ещё одно занятие, которое я давно хочу: стримы с разбором кода. Это как транслировать свои игры в твиче, только с аудиторией в 1000 раз меньше. :)))


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

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

Часть вторая, про Биологию.


Настройки программы хранятся в файлах *.trsim, и в репозитории я оставил только два использующихся для измерения производительности кода, Один с минимальным набором фич и другой в котором всё, кроме комплексной культуры и сбора статистики наоборот включено. Оба файла используются для профилирования, поэтому в них стоит галочка не использовать многопоточность. Не забудьте снять если решите заняться своими экспериментами на их основе. Попрошу ещё Михаила подкинуть парочку trsim-ов соответствующих экспериментам описанным в статье, если они у него сохранились. Примеры заполнения настроек программы использовавшиеся Марковым-старшим для статьи можно можно посмотреть в приложении к оригинальной статье, правда не в виде файла а в виде текстовых табличек, и написаных по буржуински.

Если вам интересно позапускать симуляцию с разными настройками и посмотреть какую культуру вам вырастит свой собственный легион Гетов добро пожаловать в комментарии. Выкладывайте свои trsim-ы, показывайте графики с наблюдениями и сравнениями, какие ваши изменения приводят к каким эффектам. Удалось ли воспроизвести результаты показанные Марковым к статье? Какие опыты вы бы сами хотели поставить?

Я хотел тут расписать значение всяких параметров модели, но это длинно и не очень интересно. Вместо этого поставьте брейкпоинт в функции World.SimulateYear, загрузите trsim allFeatures и пройдите один год симуляции по шагам. Сразу поймёте что там к чему и зачем.

Часть третья, про Мечты.



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

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

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

В статье Маркова и Куликова (см. обзор: "Видообразование личное дело каждого" ) описывается распространённое сейчас предположение, что оценка генетического расстояния делается по запаху молекул имунной системы и приводятся примеры других животных которые так умеют. Например рыбы колюшки и мы с вами. Некоторые исследования показывают что люди с помощью вомероназального органа способны по запаху неосознанно оценить потенциального партнёра на степень родства слишком близкие и слишком далёкие непривлекательны на запах. Есть правда и другие исследования. Тема продолжает изучаться.

Так вот, возвращаясь к мемам: комплексы взаимодополняющих и взаимно мешающих шаблонов поведения очевидно есть, и у меня есть предположение, что как и в генетике люди научились определять на сколько сильно по меметическому базису отстоит от них потенциальный пример для подражания, и соответственно этому легко перенимать его стереотипы поведения или же напротив всесторонне его мемы проверять и изучать с сильно меньшей скоростью. Предполагаю, что этот механизм позволяет при отсутствии репродуктивной изоляции поддерживать некоторую целостность мемплексов. Или наоборот некоторый небольшой процент населения может предпочитать заимствовать чужую культуру, становясь для эволюции мемов чашками петри для изолированных экспериментов по заимствованию. Хочется убедиться, что в определённых условиях такое разделение свой-чужой может быть чрезвычайно полезно. Если это так внезапно получается, что разделение людей на своих и чужих, причём именно без учёта генетической составляющей не только вредная ксенофобия, но и очень полезная особенность, нужная для развития эффективных культур. Будучи подтверждённым такое предположение имело бы огромную не только биологическую, но и культурную и политическую значимость.

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

Часть третья, про Сингулярность.


Сам Александр Владимирович оговаривается, что в их модели не получилось увидеть стремительного гиперболического разгона мощности культуры, свойственного истории человечества. Также он упомянул, что есть идея поискать условия такого роста в виде комплексов взаимно усиливающих друг друга мемов. У меня же есть несколько иное предположение. Посмотреть про сингулярность можно очень меня вдохновившее выступление Панова на GF2045. А с ещё более скептическим взглядом ознакомиться в паре статьей Сергея Карелова "Техносингулярность становится технорелигией" и её второй части.

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

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

Понятно, что чтобы это проверить, в первую очередь потребуется изменить зашитый в модель способ конкуренции за ресурсы. Сейчас буквально задано максимальное количество ресурсов, которое наши геты могут выжать из кормящей территории, и после короткого участка в самом начале эволюции развитие идёт только в том чтобы отжать больше ресурсов у соседнего племени. Реальная же палеолитическая революция радикально изменила продуктивность кормящего ландшафта и вместе с ней нашу численность. То есть с соседями надо конкурировать за контроль над условными гектарами земли, а сколько центнеров можно собрать со своего гектара определять с помощью дополнительной фичи, зависящей как от генов так и от мемов. Сейчас такой Эффективности природопользования нет, но её легко добавить в модель.

Проблема в том, что на нынешнем симуляторе этап роста численности толком не увидишь. Разумные скорости моделирования наблюдаются пока количество особей порядка тысяч, а начальная популяция обычно десятки-сотни. В реальности же наша численность на интересующем промежутке времени поменялась на 5-6 порядков. Вот что и как нужно поменять в программе чтобы можно было успешно смоделировать рост численности агентов на 5 порядков вопрос конечно интересный. С удовольствием обсужу его в комментариях. Пока у меня из идей только какие-то сомнительные группировки похожих особей. Ну или всё нафиг переписать с нуля для вычислений на GPU с помощью CUDA. В общем если кому есть что сказать добро пожаловать в комментарии.

Заключение:


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

.NET Core Взаимодействие микросервисов через Web api

19.06.2020 12:20:11 | Автор: admin

Введение



Практически все, кто имел дело с микросервисами в .NET Core, наверняка знают книгу Кристиана Хорсдала Микросервисы на платформе .NET. Здесь прекрасно описаны подходы к построению приложения на основе микросервисов, подробно рассмотрены вопросы мониторинга, журналирования, управления доступом. Единственное, чего не хватает это инструмента автоматизации взаимодействия между микросервисами.

При обычном подходе при разработке микросервиса параллельно разрабатывается web-клиент для него. И каждый раз, когда меняется web-интерфейс микросервиса, приходится затрачивать дополнительные усилия для соответствующих изменений web-клиента. Идея генерировать пару web-api/web-клиент с использованием OpenNET тоже достаточно трудоемка, хотелось бы чего-то более прозрачного для разработчика.

Итак, при альтернативном подходе в разработке нашего приложения хотелось бы:
Структуру микросервиса описывать интерфейсом с использованием атрибутов, описывающих тип метода, маршрут и способ передачи параметров, как это делается в MVC.
Функциональность микросервиса разрабатывается исключительно в классе,
реализующим этот интерфейс. Публикация конечных точек микросервиса должна быть автоматической, не требующей сложных настроек.
Web-клиент для микросервиса должен генерироваться автоматически на основе интерфейса и предоставляться через Dependency Injection.
Должен быть механизм организации редиректов на конечные точки микросервисов из главного приложения, взаимодействующего с пользовательским интерфейсом.

В соответствии с этим критериями разработан Nuget-пакет Shed.CoreKit.WebApi. В дополнение к нему создан вспомогательный пакет Shed.CoreKit.WebApi.Abstractions, содержащий атрибуты и классы, которые могут быть использованы при разработке общих проектов-сборок, где не требуется функциональность основного пакета.

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

Здесь и далее мы будем использовать следующую терминологию:
Микросервис приложение (проект) ASP.NET Core, которое может запускаться консольно, под Internet Information Services (IIS) или в Docker-контейнере.
Интерфейс сущность .NET, набор методов и свойств без реализации.
Конечная точка путь к корню приложения микросервиса или реализации интерфейса. Примеры: localhost:5001, localhost:5000/products
Маршрут путь к методу интерфейса от конечной точки. Может определяться по умолчанию так же как в MVC или устанавливаться при помощи атрибута.

Структура приложения MicroCommerce



  1. ProductCatalog микросервис, предоставляющий сведения о продуктах.
  2. ShoppingCart микросервис, предоставляющий сведения о покупках пользователя, а также возможность добавлять/удалять покупки. При изменении состояния корзины пользователя генерируются события для уведомления других микросервисов.
  3. ActivityLogger микросервис, собирающий сведения о событиях других микросервисов. Предоставляет конечную точку для получения логов.
  4. WebUI Пользовательский интерфейс приложения, должен быть реализован в виде Single Page Application.
  5. Interfaces интерфейсы микросервисов и классы-модели.
  6. Middleware общая функциональность для всех микросервисов


Разработка приложения MicroCommerce




Создаем пустое решение .Net Core. Добавляем в него проект WebUI как пустой ASP.NET Core WebApplication. Далее добавляем проекты микросервисов ProductCatalog, ShoppingCart, ActivityLog, также как пустые проекты ASP.NET Core WebApplication. В заключение добавляем две библиотеки классов Interfaces и Middleware.

1. Interfaces интерфейсы микросервисов и классы-модели



Подключаем к проекту Nuget-пакет Shed.CoreKit.WebApi.Abstractions.

Добавляем интерфейс IProductCatalog и модели для него:
//// Interfaces/IProductCatalog.cs//using MicroCommerce.Models;using Shed.CoreKit.WebApi;using System;using System.Collections.Generic;namespace MicroCommerce{    public interface IProductCatalog    {        IEnumerable<Product> Get();        [Route("get/{productId}")]        public Product Get(Guid productId);    }}


//// Interfaces/Models/Product.cs//using System;namespace MicroCommerce.Models{    public class Product    {        public Guid Id { get; set; }        public string Name { get; set; }        public Product Clone()        {            return new Product            {                Id = Id,                Name = Name            };        }    }}

Использование атрибута Route ничем не отличается от аналогичного в ASP.NET Core MVC, но нужно помнить, что этот атрибут должен быть из namespace Shed.CoreKit.WebApi, и никакого другого. То же самое касается атрибутов HttpGet, HttpPut, HttpPost, HttpPatch, HttpDelete, а также FromBody в случае их применения.
Правила применения атрибутов типа Http[Methodname] такие же, как в MVC, то есть если префикс имени метода интерфейса совпадает с именем требуемого Http-метода, то не нужно его дополнительно определять, иначе используем соответствующий атрибут.
Атрибут FromBody применяется к параметру метода, если этот параметр должен извлекаться из тела запроса. Замечу, что как и ASP.NET Core MVC, его нужно указывать всегда, никаких правил по умолчанию нет. И в параметрах метода может быть только один параметр с этим атрибутом.

Добавляем интерфейс IShoppingCart и модели для него
//// Interfaces/IShoppingCart.cs//using MicroCommerce.Models;using Shed.CoreKit.WebApi;using System;using System.Collections.Generic;namespace MicroCommerce{    public interface IShoppingCart    {        Cart Get();        [HttpPut, Route("addorder/{productId}/{qty}")]        Cart AddOrder(Guid productId, int qty);        Cart DeleteOrder(Guid orderId);        [Route("getevents/{timestamp}")]        IEnumerable<CartEvent> GetCartEvents(long timestamp);    }}


//// Interfaces/IProductCatalog/Order.cs//using System;namespace MicroCommerce.Models{    public class Order    {        public Guid Id { get; set; }        public Product Product { get; set; }        public int Quantity { get; set; }        public Order Clone()        {            return new Order            {                Id = Id,                Product = Product.Clone(),                Quantity = Quantity            };        }    }}


//// Interfaces/Models/Cart.cs//using System;namespace MicroCommerce.Models{    public class Cart    {        public IEnumerable<Order> Orders { get; set; }    }}


//// Interfaces/Models/CartEvent.cs//using System;namespace MicroCommerce.Models{    public class CartEvent: EventBase    {        public CartEventTypeEnum Type { get; set; }        public Order Order { get; set; }    }}


//// Interfaces/Models/CartEventTypeEnum.cs//using System;namespace MicroCommerce.Models{    public enum CartEventTypeEnum    {        OrderAdded,        OrderChanged,        OrderRemoved    }}


//// Interfaces/Models/EventBase.cs//using System;namespace MicroCommerce.Models{    public abstract class EventBase    {        private static long TimestampBase;        static EventBase()        {            TimestampBase = new DateTime(2000, 1, 1).Ticks;        }        public long Timestamp { get; set; }                public DateTime Time { get; set; }        public EventBase()        {            Time = DateTime.Now;            Timestamp = Time.Ticks - TimestampBase;        }    }}

Пара слов о базовом типе событий EventBase. При публикации событий используем подход, описанный в книге, т.е. любое событие содержит метку времени создания Timestamp, при опросе источника события слушатель передает последний полученный timestamp. К сожалению, тип long некорректно преобразуется в в тип Number javascript при больших значениях, поэтому мы используем некую хитрость вычитаем timestamp базовой даты (Timestamp = Time.Ticks TimestampBase). Конкретное значение базовой даты абсолютно неважно.

Добавляем интерфейс IActivityLogger и модели для него
//// Interfaces/IActivityLogger.cs//using MicroCommerce.Models;using System.Collections.Generic;namespace MicroCommerce{    public interface IActivityLogger    {        IEnumerable<LogEvent> Get(long timestamp);    }}


//// Interfaces/Models/LogEvent.cs//namespace MicroCommerce.Models{    public class LogEvent: EventBase    {        public string Description { get; set; }    }}


2. Микросервис ProductCatalog


Открываем Properties/launchSettings.json, привязываем проект к порту 5001.
{  "iisSettings": {    "windowsAuthentication": false,    "anonymousAuthentication": true,    "iisExpress": {      "applicationUrl": "http://localhost:60670",      "sslPort": 0    }  },  "profiles": {    "MicroCommerce.ProductCatalog": {      "commandName": "Project",      "environmentVariables": {        "ASPNETCORE_ENVIRONMENT": "Development"      },      "applicationUrl": "http://localhost:5001"    }  }}

Подключаем к проекту Nuget- пакет Shed.CoreKit.WebApi и ссылки на проекты Interfaces и Middleware. О Middleware будет более подробно рассказано ниже.

Добавляем реализацию интерфейса IProductCatalog
//// ProductCatalog/ProductCatalog.cs//using MicroCommerce.Models;using System;using System.Collections.Generic;using System.Linq;namespace MicroCommerce.ProductCatalog{    public class ProductCatalogImpl : IProductCatalog    {        private Product[] _products = new[]        {            new Product{ Id = new Guid("6BF3A1CE-1239-4528-8924-A56FF6527595"), Name = "T-shirt" },            new Product{ Id = new Guid("6BF3A1CE-1239-4528-8924-A56FF6527596"), Name = "Hoodie" },            new Product{ Id = new Guid("6BF3A1CE-1239-4528-8924-A56FF6527597"), Name = "Trousers" }        };        public IEnumerable<Product> Get()        {            return _products;        }        public Product Get(Guid productId)        {            return _products.FirstOrDefault(p => p.Id == productId);        }    }}


Каталог продуктов храним в статическом поле, для упрощения примера. Конечно же, в реальном приложении нужно использовать какое-то другое хранилище, которое можно получить как зависимость через Dependency Injection.

Теперь эту реализацию нужно подключить как конечную точку. Если бы мы использовали традиционный подход, мы должны были бы использовать инфраструктуру MVC, то есть создать контроллер, передать ему нашу реализацию как зависимость, настроить роутинг и т.д. С использованием Nuget-пакета Shed.CoreKit.WebApi это делается гораздо проще. Достаточно зарегистрировать нашу реализацию в Dependency Injection (services.AddTransient<IProductCatalog, ProductCatalogImpl>()), затем объявляем ее как конечную точку (app.UseWebApiEndpoint()) при помощи метода-расширителя UseWebApiEndpoint из пакета Shed.CoreKit.WebApi. Это делается в Setup

//// ProductCatalog/Setup.cs//using MicroCommerce.Middleware;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Microsoft.Extensions.Logging;using Shed.CoreKit.WebApi;namespace MicroCommerce.ProductCatalog{    public class Startup    {        public void ConfigureServices(IServiceCollection services)        {            services.AddCorrelationToken();            services.AddCors();            // регистрируем реализацию как зависимость в контейнере IoC            services.AddTransient<IProductCatalog, ProductCatalogImpl>();            services.AddLogging(builder => builder.AddConsole());            services.AddRequestLogging();        }        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)        {            if (env.IsDevelopment())            {                app.UseDeveloperExceptionPage();            }            app.UseCorrelationToken();            app.UseRequestLogging();            app.UseCors(builder =>            {                builder                    .AllowAnyOrigin()                    .AllowAnyMethod()                    .AllowAnyHeader();            });            // привязываем реализацию к конечной точке            app.UseWebApiEndpoint<IProductCatalog>();        }    }}


Это приводит к тому, что в микросервисе появляются методы:
localhost:5001/get
localhost:5001/get/Метод UseWebApiEndpoint может принимать необязательный параметр root.
Если мы подключим конечную точку таким образом: app.UseWebApiEndpoint(products)
то конечная точка микросервиса будет выглядеть вот так:
localhost:5001/products/get
Это может быть полезно, если у нас появится необходимость подключить к микросервису несколько интерфейсов.

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

Остальной код в Setup настраивает и подключает дополнительные возможности.
Пара services.AddCors() / app.UseCors(...) разрешает использование кросс-доменных запросов в проекте. Это необходимо при редиректах запросов со стороны UI.
Пара services.AddCorrelationToken() / app.UseCorrelationToken() подключает использование токенов корреляции при журналировании запросов, как это описано в книге Кристиана Хорсдала. Мы дополнительно обсудим это позже.
И наконец, пара services.AddRequestLogging() / app.UseRequestLogging() подключает журналирование запросов из проекта Middleware. К этому тоже вернемся позже.

3. Микросервис ShoppingCart


Привязываем проект к порту 5002 аналогично ProductCatalog.

Подключаем к проекту Nuget- пакет Shed.CoreKit.WebApi и ссылки на проекты Interfaces и Middleware.

Добавляем реализацию интерфейса IShoppingCart.
//// ShoppingCart/ShoppingCart.cs//using MicroCommerce.Models;using System;using System.Collections.Generic;using System.Linq;namespace MicroCommerce.ShoppingCart{    public class ShoppingCartImpl : IShoppingCart    {        private static List<Order> _orders = new List<Order>();        private static List<CartEvent> _events = new List<CartEvent>();        private IProductCatalog _catalog;        public ShoppingCartImpl(IProductCatalog catalog)        {            _catalog = catalog;        }        public Cart AddOrder(Guid productId, int qty)        {            var order = _orders.FirstOrDefault(i => i.Product.Id == productId);            if(order != null)            {                order.Quantity += qty;                CreateEvent(CartEventTypeEnum.OrderChanged, order);            }            else            {                var product = _catalog.Get(productId);                if (product != null)                {                    order = new Order                    {                        Id = Guid.NewGuid(),                        Product = product,                        Quantity = qty                    };                    _orders.Add(order);                    CreateEvent(CartEventTypeEnum.OrderAdded, order);                }            }            return Get();        }        public Cart DeleteOrder(Guid orderId)        {            var order = _orders.FirstOrDefault(i => i.Id == orderId);            if(order != null)            {                _orders.Remove(order);                CreateEvent(CartEventTypeEnum.OrderRemoved, order);            }            return Get();        }        public Cart Get()        {            return new Cart            {                Orders = _orders            };        }        public IEnumerable<CartEvent> GetCartEvents(long timestamp)        {            return _events.Where(e => e.Timestamp > timestamp);        }        private void CreateEvent(CartEventTypeEnum type, Order order)        {            _events.Add(new CartEvent            {                Timestamp = DateTime.Now.Ticks,                Time = DateTime.Now,                Order = order.Clone(),                Type = type            });        }    }}

Здесь, как и в ProductCatalog, используем статические поля как хранилища. Но этот микросервис еще использует вызовы к ProductCatalog для получения информации о продукте, поэтому ссылку на IProductCatalog передаем в конструктор как зависимость.
Теперь эту зависимость нужно определить в DI, и мы используем для этого метод-расширитель AddWebApiEndpoints из пакета Shed.CoreKit.WebApi. Этот метод регистрирует в DI фабрику-генератор WebApi-клиентов для интерфейса IProductCatalog.
При генерировании WebApi-клиента фабрика использует зависимость System.Net.Http.HttpClient. Если в приложении требуются какие-то специальные настройки для HttpClient (учетные данные, специальные заголовки/токены), это можно сделать при регистрации HttpClient в DI.

//// ShoppingCart/Settings.cs//using MicroCommerce.Middleware;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Microsoft.Extensions.Logging;using Shed.CoreKit.WebApi;using System.Net.Http;namespace MicroCommerce.ShoppingCart{    public class Startup    {        public void ConfigureServices(IServiceCollection services)        {            services.AddCorrelationToken();            services.AddCors();            services.AddTransient<IShoppingCart, ShoppingCartImpl>();            services.AddTransient<HttpClient>();            services.AddWebApiEndpoints(new WebApiEndpoint<IProductCatalog>(new System.Uri("http://localhost:5001")));            services.AddLogging(builder => builder.AddConsole());            services.AddRequestLogging();        }        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)        {            if (env.IsDevelopment())            {                app.UseDeveloperExceptionPage();            }            app.UseCorrelationToken();            app.UseRequestLogging("getevents");            app.UseCors(builder =>            {                builder                    .AllowAnyOrigin()                    .AllowAnyMethod()                    .AllowAnyHeader();            });            app.UseWebApiEndpoint<IShoppingCart>();        }    }}

Метод AddWebApiEndpoints может принимать произвольное количество параметров, поэтому возможно настроить все зависимости одним вызовом этого метода.
В остальном все настройки аналогичны ProductCatalog.

4. Микросервис ActivityLogger


Привязываем проект к порту 5003 аналогично ProductCatalog.

Подключаем к проекту Nuget- пакет Shed.CoreKit.WebApi и ссылки на проекты Interfaces и Middleware.

Добавляем реализацию интерфейса IActivityLogger.
//// ActivityLogger/ActivityLogger.cs//using MicroCommerce;using MicroCommerce.Models;using System.Collections.Generic;using System.Linq;namespace ActivityLogger{    public class ActivityLoggerImpl : IActivityLogger    {        private IShoppingCart _shoppingCart;        private static long timestamp;        private static List<LogEvent> _log = new List<LogEvent>();        public ActivityLoggerImpl(IShoppingCart shoppingCart)        {            _shoppingCart = shoppingCart;        }        public IEnumerable<LogEvent> Get(long timestamp)        {            return _log.Where(i => i.Timestamp > timestamp);        }        public void ReceiveEvents()        {            var cartEvents = _shoppingCart.GetCartEvents(timestamp);            if(cartEvents.Count() > 0)            {                timestamp = cartEvents.Max(c => c.Timestamp);                _log.AddRange(cartEvents.Select(e => new LogEvent                {                    Description = $"{GetEventDesc(e.Type)}: '{e.Order.Product.Name} ({e.Order.Quantity})'"                }));            }        }        private string GetEventDesc(CartEventTypeEnum type)        {            switch (type)            {                case CartEventTypeEnum.OrderAdded: return "order added";                case CartEventTypeEnum.OrderChanged: return "order changed";                case CartEventTypeEnum.OrderRemoved: return "order removed";                default: return "unknown operation";            }        }    }}

Здесь также используется зависимость от другого микросервиса (IShoppingCart). Но одна из задач этого сервиса слушать события других сервисов, поэтому добавляем дополнительный метод ReceiveEvents(), который будем вызывать из планировщика. Мы его добавим к проекту дополнительно.
//// ActivityLogger/Scheduler.cs//using Microsoft.Extensions.Hosting;using System;using System.Threading;using System.Threading.Tasks;namespace ActivityLogger{    public class Scheduler : BackgroundService    {        private IServiceProvider ServiceProvider;        public Scheduler(IServiceProvider serviceProvider)        {            ServiceProvider = serviceProvider;        }        protected override Task ExecuteAsync(CancellationToken stoppingToken)        {            Timer timer = new Timer(new TimerCallback(PollEvents), stoppingToken, 2000, 2000);            return Task.CompletedTask;        }        private void PollEvents(object state)        {            try            {                var logger = ServiceProvider.GetService(typeof(MicroCommerce.IActivityLogger)) as ActivityLoggerImpl;                logger.ReceiveEvents();            }            catch            {            }        }    }}

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

//// ActivityLogger/Setup.cs//using System.Net.Http;using MicroCommerce;using MicroCommerce.Middleware;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Microsoft.Extensions.Logging;using Shed.CoreKit.WebApi;namespace ActivityLogger{    public class Startup    {        public void ConfigureServices(IServiceCollection services)        {            services.AddCorrelationToken();            services.AddCors();            services.AddTransient<IActivityLogger, ActivityLoggerImpl>();            services.AddTransient<HttpClient>();            services.AddWebApiEndpoints(new WebApiEndpoint<IShoppingCart>(new System.Uri("http://localhost:5002")));            // регистрируем планировщик (запустится при старте приложения, больше ничего делать не нужно)            services.AddHostedService<Scheduler>();            services.AddLogging(builder => builder.AddConsole());            services.AddRequestLogging();        }        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)        {            if (env.IsDevelopment())            {                app.UseDeveloperExceptionPage();            }            app.UseCorrelationToken();            app.UseRequestLogging("get");            app.UseCors(builder =>            {                builder                    .AllowAnyOrigin()                    .AllowAnyMethod()                    .AllowAnyHeader();            });            app.UseWebApiEndpoint<IActivityLogger>();        }    }}


5. WebUI пользовательский интерфейс.


Привязываем проект к порту 5000 аналогично ProductCatalog.

Подключаем к проекту Nuget- пакет Shed.CoreKit.WebApi. Cсылки на проекты Interfaces и Middleware нужно подключать только в том случае, если мы в этом проекте собираемся использовать вызовы к микросервисам

Строго говоря, это обычный ASP.NET проект и в нем возможно использование MVC, т.е. для взаимодействия с UI мы можем создать контроллеры, которые используют наши интерфейсы микросервисов как зависимости. Но интереснее и практичнее оставить за этим проектом только предоставление пользовательского интерфейса, а все обращения со стороны UI перенаправлять непосредственно микросервисам. Для этого используется метод-расширитель UseWebApiRedirect из пакета Shed.CoreKit.WebApi
//// WebUI/Setup.cs//using MicroCommerce.Interfaces;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Shed.CoreKit.WebApi;using System.Net.Http;namespace MicroCommerce.Web{    public class Startup    {        public void ConfigureServices(IServiceCollection services)        {        }        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)        {            if (env.IsDevelopment())            {                app.UseDeveloperExceptionPage();            }            app.Use(async (context, next) =>            {                //  when root calls, the start page will be returned                if(string.IsNullOrEmpty(context.Request.Path.Value.Trim('/')))                {                    context.Request.Path = "/index.html";                }                await next();            });            app.UseStaticFiles();            // редиректы на микросервисы            app.UseWebApiRedirect("api/products", new WebApiEndpoint<IProductCatalog>(new System.Uri("http://localhost:5001")));            app.UseWebApiRedirect("api/orders", new WebApiEndpoint<IShoppingCart>(new System.Uri("http://localhost:5002")));            app.UseWebApiRedirect("api/logs", new WebApiEndpoint<IActivityLogger>(new System.Uri("http://localhost:5003")));        }    }}

Все очень просто. Теперь если со стороны UI придет, например, запрос к http://localhost:5000/api/products/get, он будет автоматически перенаправлен на http://localhost:5001/get. Конечно же, для этого микросервисы должны разрешать кросс-доменные запросы, но мы разрешили это ранее (см. CORS в реализации микросервисов).

Теперь осталось только разработать пользовательский интерфейс, и лучше всего для этого подходит Single Page Application. Можно использовать Angular или React, но мы просто создадим маленькую страничку с использованием готовой темы bootstrap и фреймворка knockoutjs.
<!DOCTYPE html><!-- WebUI/wwwroot/index.html --><html><head>    <meta charset="utf-8" />    <title></title>    <link rel="stylesheet" href="http://personeltest.ru/aways/cdnjs.cloudflare.com/ajax/libs/bootswatch/4.5.0/materia/bootstrap.min.css" />"    <style type="text/css">        body {            background-color: #0094ff;        }        .panel {            background-color: #FFFFFF;            margin-top:20px;            padding:10px;            border-radius: 4px;        }        .table .desc {            vertical-align: middle;            font-weight:bold;        }        .table .actions {            text-align:right;            white-space:nowrap;            width:40px;        }    </style>    <script src="http://personeltest.ru/aways/code.jquery.com/jquery-3.5.1.min.js"            integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="            crossorigin="anonymous"></script>    <script src="http://personeltest.ru/aways/cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"></script>    <script src="../index.js"></script></head><body>    <div class="container">        <div class="row">            <div class="col-12">                <div class="panel panel-heading">                    <div class="panel-heading">                        <h1>MicroCommerce</h1>                    </div>                </div>            </div>            <div class="col-xs-12 col-md-6">                <div class="panel panel-default">                    <h2>All products</h2>                    <table class="table table-bordered" data-bind="foreach:products">                        <tr>                            <td data-bind="text:name"></td>                            <td class="actions">                                <a class="btn btn-primary" data-bind="click:function(){$parent.addorder(id, 1);}">ADD</a>                            </td>                        </tr>                    </table>                </div>            </div>            <div class="col-xs-12 col-md-6">                <div class="panel panel-default" data-bind="visible:shoppingCart()">                    <h2>Shopping cart</h2>                    <table class="table table-bordered" data-bind="foreach:shoppingCart().orders">                        <tr>                            <td data-bind="text:product.name"></td>                            <td class="actions" data-bind="text:quantity"></td>                            <td class="actions">                                <a class="btn btn-primary" data-bind="click:function(){$parent.delorder(id);}">DELETE</a>                            </td>                        </tr>                    </table>                </div>            </div>            <div class="col-12">                <div class="panel panel-default">                    <h2>Operations history</h2>                    <!-- ko foreach:logs -->                    <div class="log-item">                        <span data-bind="text:time"></span>                        <span data-bind="text:description"></span>                    </div>                    <!-- /ko -->                </div>            </div>        </div>    </div>    <script src="http://personeltest.ru/aways/stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>    <script>        var model = new IndexModel();        ko.applyBindings(model);    </script></body></html>


//// WebUI/wwwroot/index.js//function request(url, method, data) {    return $.ajax({        cache: false,        dataType: 'json',        url: url,        data: data ? JSON.stringify(data) : null,        method: method,        contentType: 'application/json'    });}function IndexModel() {    this.products = ko.observableArray([]);    this.shoppingCart = ko.observableArray(null);    this.logs = ko.observableArray([]);    var _this = this;    this.getproducts = function () {        request('/api/products/get', 'GET')            .done(function (products) {                _this.products(products);                console.log("get products: ", products);            }).fail(function (err) {                console.log("get products error: ", err);            });    };    this.getcart = function () {        request('/api/orders/get', 'GET')            .done(function (cart) {                _this.shoppingCart(cart);                console.log("get cart: ", cart);            }).fail(function (err) {                console.log("get cart error: ", err);            });    };    this.addorder = function (id, qty) {        request(`/api/orders/addorder/${id}/${qty}`, 'PUT')            .done(function (cart) {                _this.shoppingCart(cart);                console.log("add order: ", cart);            }).fail(function (err) {                console.log("add order error: ", err);            });    };    this.delorder = function (id) {        request(`/api/orders/deleteorder?orderId=${id}`, 'DELETE')            .done(function (cart) {                _this.shoppingCart(cart);                console.log("del order: ", cart);            }).fail(function (err) {                console.log("del order error: ", err);            });    };    this.timestamp = Number(0);    this.updateLogsInProgress = false;    this.updatelogs = function () {        if (_this.updateLogsInProgress)            return;        _this.updateLogsInProgress = true;        request(`/api/logs/get?timestamp=${_this.timestamp}`, 'GET')            .done(function (logs) {                if (!logs.length) {                    return;                }                ko.utils.arrayForEach(logs, function (item) {                    _this.logs.push(item);                    _this.timestamp = Math.max(_this.timestamp, Number(item.timestamp));                });                console.log("update logs: ", logs, _this.timestamp);            }).fail(function (err) {                console.log("update logs error: ", err);            }).always(function () { _this.updateLogsInProgress = false; });    };    this.getproducts();    this.getcart();    this.updatelogs();    setInterval(() => _this.updatelogs(), 1000);}

Я не буду подробно объяснять реализацию UI, т.к. это выходит за рамки темы статьи, скажу только, что в javascript-модели определены свойства и коллекции для привязки со стороны HTML-разметки, а также функции, реагирующие на нажатие кнопок для обращения к конечным точкам WebApi, которые незаметно для разработчика перенаправляются к соответствующим микросервисам. Как выглядит пользовательский интерфейс и как он работает мы рассмотрим позднее в разделе Тестирование приложения.

6. Несколько слов об общей функциональности


Мы не затронули в этой статье некоторые другие аспекты разработки приложения, такие как журналирование, мониторинг работоспособности, аутентификация и авторизация. Это все подробно рассмотрено в книге Кристиана Хорсдала и вполне применимо в рамках вышеописанного подхода. Вместе с тем эти аспекты слишком специфичны для для каждого конкретного приложения и не имеет смысла выносить их в Nuget-пакет, лучше просто создать отдельную сборку в рамках приложения. Мы такую сборку создали это Middleware. Для примера просто добавим сюда функциональность для журналирования запросов, которую мы уже подключили при разработке микросервисов (см. пп. 2-4).
//// Middleware/RequestLoggingExt.cs//using Microsoft.AspNetCore.Builder;using Microsoft.Extensions.DependencyInjection;namespace MicroCommerce.Middleware{    public static class RequestLoggingExt    {        private static RequestLoggingOptions Options = new RequestLoggingOptions();        public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder, params string[] exclude)        {            Options.Exclude = exclude;            return builder.UseMiddleware<RequestLoggingMiddleware>();        }        public static IServiceCollection AddRequestLogging(this IServiceCollection services)        {            return services.AddSingleton(Options);        }    }    internal class RequestLoggingMiddleware    {        private readonly RequestDelegate _next;        private readonly ILogger _logger;        private RequestLoggingOptions _options;        public RequestLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, RequestLoggingOptions options)        {            _next = next;            _options = options;            _logger = loggerFactory.CreateLogger("LoggingMiddleware");        }        public async Task InvokeAsync(HttpContext context)        {            if(_options.Exclude.Any(i => context.Request.Path.Value.Trim().ToLower().Contains(i)))            {                await _next.Invoke(context);                return;            }            var request = context.Request;            _logger.LogInformation($"Incoming request: {request.Method}, {request.Path}, [{HeadersToString(request.Headers)}]");            await _next.Invoke(context);            var response = context.Response;            _logger.LogInformation($"Outgoing response: {response.StatusCode}, [{HeadersToString(response.Headers)}]");        }        private string HeadersToString(IHeaderDictionary headers)        {            var list = new List<string>();            foreach(var key in headers.Keys)            {                list.Add($"'{key}':[{string.Join(';', headers[key])}]");            }            return string.Join(", ", list);        }    }    internal class RequestLoggingOptions    {        public string[] Exclude = new string[] { };    }}

Пара методов AddRequestLogging() / UseRequestLogging(...) позволяет включить журналирование запросов в микросервисе. Метод UseRequestLogging кроме того может принимать произвольное количество путей-исключений. Мы воспользовались этим в ShoppingCart и в ActivityLogger чтобы исключить из журналирования опросы событий и избежать переполнения логов. Но повторюсь, журналирование, как и любая другай общая функциональность это исключительно зона ответственности разработчиков и реализуется в рамках конкретного проекта.

Тестирование приложения


Запускаем решение, видим слева список продуктов для добавления в корзину, пустую корзину справа и историю операций снизу, тоже пока пустую.


В консолях микросервисов мы видим, что при старте UI уже запросил и получил некоторые данные. Например, для получения списка продуктов был отправлен запрос localhost:5000/api/products/get, который был перенаправлен на localhost:5001/get.





Когда мы нажимаем кнопку ADD, в корзину добавляется соответствующий продукт. Если продукт был уже ранее добавлен, просто увеличивается количество.



Микросервису ShoppingCart отправляется запрос localhost:5002/addorder/

Но поскольку ShoppingCart не хранит список продуктов, сведения о заказанном продукте он получает от ProductCatalog.



Обратите внимание, перед отправкой запроса к ProductCatalog был назначен токен корреляции. Это позволяет отследить цепочки связанных запросов в случае сбоев.

По завершении операции ShoppingCart публикует событие, которое отслеживает и регистрирует ActivityLogger. В свою очередь, UI периодически опрашивает этот микросервис и отображает полученные данные на панели истории операций. Конечно же, записи в истории появляются с некоторой задержкой, т.к. это параллельный, не зависящий от операции добавления продукта механизм.

Заключение


Nuget-пакет Shed.CoreKit.WebApi позволяет:
  • полностью сосредоточиться на разработке бизнес-логики приложения, не прилагая дополнительных усилий на вопросы взаимодействия микросервисов;
  • описывать структуру микросервиса интерфейсом .NET и использовать его как при разработке самого микросервиса, так и для генерации Web-клиента (Web-клиент для микросервиса генерируется фабричным методом после регистрации интерфейса в DI и предоставляется как зависимость);
  • регистрировать интерфейсы микросервисов как зависимости в Dependency Injection;
  • организовать перенаправление запросов со стороны Web UI к микросервисам без дополнительных усилий при разработке UI.
Подробнее..

System.Threading.Channels высокопроизводительный производитель-потребитель и асинхронность без алокаций и стэк дайва

07.07.2020 14:13:48 | Автор: admin
И снова здравствуй. Какое-то время назад я писал о другом малоизвестном инструменте для любителей высокой производительности System.IO.Pipelines. По своей сути, рассматриваемый System.Threading.Channels (в дальнейшем каналы) построен по похожим принципам, что и Пайплайны, решает ту же задачу Производитель-Потребитель. Однако имеет в разы более простое апи, которое изящно вольется в любого рода enterprise-код. При этом использует асинхронность без алокаций и без stack-dive даже в асинхронном случае! (Не всегда, но часто).



Оглавление




Введение


Задача Производитель/Потребитель встречается на пути программистов довольно часто и уже не первый десяток лет. Сам Эдсгер Дейкстра приложил руку к решению данной задачи ему принадлежит идея использования семафоров для синхронизации потоков при организации работы по принципу производитель/потребитель. И хотя ее решение в простейшем виде известно и довольно тривиально, в реальном мире данный шаблон (Производитель/Потребитель) может встречаться в гораздо более усложненном виде. Также современные стандарты программирования накладывают свои отпечатки, код пишется более упрощенно и разбивается для дальнейшего переиспользования. Все делается для понижения порога написания качественного кода и упрощения данного процесса. И рассматриваемое пространство имен System.Threading.Channels очередной шаг на пути к этой цели.

Какое-то время назад я рассматривал System.IO.Pipelines. Там требовалось более внимательная работа и глубокое осознание дела, в ход шли Span и Memory, а для эффективной работы требовалось не вызывать очевидных методов (чтобы избежать лишних выделений памяти) и постоянно думать в байтах. Из-за этого программный интерфейс Пайплайнов был нетривиален и не интуитивно понятен.

В System.Threading.Channels пользователю представляется гораздо более простое api для работы. Стоит упомянуть, что несмотря на простоту api, данный инструмент является весьма оптимизированным и на протяжении своей работы вполне вероятно не выделит память. Возможно это благодаря тому, что под капотом повсеместно используется ValueTask, а даже в случае реальной асинхронности используется IValueTaskSource, который переиспользуется для дальнейших операций. Именно в этом заключается весь интерес реализации Каналов.

Каналы являются обобщенными, тип обобщения, как несложно догадаться тип, экземпляры которого будут производиться и потребляться. Интересно то, что реализация класса Channel, которая помещается в 1 строку (источник github):

namespace System.Threading.Channels{    public abstract class Channel<T> : Channel<T, T> { }}

Таким образом основной класс каналов параметризован 2 типами отдельно под канал производитель и канал потребитель. Но для реализованых каналов это не используется.
Для тех, кто знаком с Пайплайнами, общий подход для начала работы покажется знакомым. А именно. Мы создаем 1 центральный класс, из которого вытаскиваем отдельно производителей(CannelWriter) и потребителей(ChannelReader). Несмотря на названия, стоит помнить, что это именно производитель/потребитель, а не читатель/писатель из еще одной классической одноименной задачи на многопоточность. ChannelReader изменяет состояние общего channel (вытаскивает значение), которое более становится недоступно. А значит он скорее не читает, а потребляет. Но с реализацией мы ознакомимся позже.

Начало работы. Channel


Начало работы с каналами начинается с абстрактного класса Channel<T> и статического класса Channel, который создает наиболее подходящую реализацию. Далее из этого общего Channel можно получать ChannelWriter для записи в канал и ChannelReader для потребления из канала. Канал является хранилищем общей информации для ChannelWriter и ChannelReader, так, именно в нем хранятся все данные. А уже логика их записи или потребления рассредоточения в ChannelWriter и ChannelReader, Условно каналы можно разделить на 2 группы безграничные и ограниченные. Первые более простые по реализации, в них можно писать безгранично (пока память позволяет). Вторые же ограничены неким максимальным значением количества записей.

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

Поведения читателей по большей части одинаково если в канале есть что-то, то читатель просто читает это и завершается синхронно. Если ничего нет, то ожидает пока кто-то что-то запишет.

Статический класс Channel содержит 4 метода для создания вышеперечисленных каналов:

Channel<T> CreateUnbounded<T>();Channel<T> CreateUnbounded<T>(UnboundedChannelOptions options);Channel<T> CreateBounded<T>(int capacity);Channel<T> CreateBounded<T>(BoundedChannelOptions options);

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

UnboundedChannelOptions содержит 3 свойства, значение которых по умолчанию false:

  1. AllowSynchronousContinuations просто сумасводящая опция, которая позволяет выполнить продолжение асинхронной операции тому, кто ее разблокирует. А теперь по-простому. Допустим, мы писали в заполненный канал. Соответственно, операция прерывается, поток освобождается, а продолжение будет выполнено по завершению на новом потоке из пула. Но если включить эту опцию, продолжение выполнит тот, кто разблокирует операцию, то есть в нашем случае читатель. Это серьезно меняет внутреннее поведение и позволяет более экономно и производительно распоряжаться ресурсами, ведь зачем нам слать какие-то продолжения в какие-то потоки, если мы можем сами его выполнить;
  2. SingleReader указывает, что будет использоваться один потребитель. Опять же, это позволяет избавиться от некоторой лишней синхронизации;
  3. SingleWriter то же самое, только для писателя;

BoundedChannelOptions содержит те же 3 свойства и еще 2 сверху

  1. AllowSynchronousContinuations то же;
  2. SingleReader то же;
  3. SingleWriter то же;
  4. Capacity количество вмещаемых в канал записей. Данный параметр также является параметром конструктора;
  5. FullMode перечисление BoundedChannelFullMode, которое имеет 4 опции, определяет поведение при попытке записи в заполненный канал:
    • Wait ожидает освобождения места для завершения асинхронной операции
    • DropNewest записываемый элемент перезаписывает самый новый из существующих, завершается синхронно
    • DropOldest записываемый элемент перезаписывает самый старый из существующих завершается синхронно
    • DropWrite записываемый элемент не записывается, завершается синхронно


В зависимости от переданных параметров и вызванного метода будет создана одна из 3 реализаций: SingleConsumerUnboundedChannel, UnboundedChannel, BoundedChannel. Но это не столь важно, ведь пользоваться каналом мы будем через базовый класс Channel<TWrite, TRead>.

У него есть 2 свойства:

  • ChannelReader<TRead> Reader { get; protected set; }
  • ChannelWriter<TWrite> Writer { get; protected set; }

А также, 2 оператора неявного приведения типа к ChannelReader<TRead> и ChannelWriter<TWrite>.

Пример начала работы с каналами:

Channel<int> channel = Channel.CreateUnbounded<int>();//Можно делать такChannelWriter<int> writer = channel.Writer;ChannelReader<int> reader = channel.Reader; //Или такChannelWriter<int> writer = channel;ChannelReader<int> reader = channel;

Данные хранятся в очереди. Для 3 типов используются 3 разные очереди ConcurrentQueue<T>, Deque<T> и SingleProducerSingleConsumerQueue<T>. На этом моменте мне показалось, что я устарел и пропустил кучу новых простейших коллекций. Но спешу огорчить они не для всех. Помечены internal, так что использовать их не получится. Но если вдруг они понадобятся на проде их можно найти здесь (SingleProducerConsumerQueue) и здесь (Deque). Реализация последней весьма проста. Советую ознакомится, ее очень быстро можно изучить.

Итак, приступим к изучению непосредственно ChannelReader и ChannelWriter, а также интересных деталей реализации. Они все сводятся к асинхронности без выделений памяти с помощью IValueTaskSource.

ChannelReader потребитель


При запросе объекта потребителя возвращается одна из реализаций абстрактного класса ChannelReader<T>. Опять же в отличие от Пайплайнов АПИ несложное и методов немного. Достаточно просто знать список методов, чтобы понять, как использовать это на практике.

Методы:

  1. Виртуальное get-only свойство Task Completion { get; }
    Обьект типа Task, который завершается, когда закрывается канал;
  2. Виртуальное get-only свойство int Count { get; }
    Тут сделает заострить внимание, что возвращается текущее количество доступных для чтения объектов;
  3. Виртуальное get-only свойство bool CanCount { get; }
    Показывает, доступно ли свойство Count;
  4. Абстрактный метод bool TryRead(out T item)
    Пытается потребить объект из канала. Возвращает bool, показывающий, получилось ли у него прочитать. Результат помещается в out параметр (или null, если не получилось);
  5. Абстрактный ValueTask<bool> WaitToReadAsync(CancellationToken cancellationToken = default)
    Возвращается ValueTask со значением true, когда в канале появятся доступные для чтения данные, до тех пор задача не завершается. Возвращает ValueTask со значением false, когда канал закрывается(данных для чтения больше не будет);
  6. Виртуальный метод ValueTask<T> ReadAsync(CancellationToken cancellationToken = default)
    Потребляет значение из канала. Если значение есть, возвращается синхронно. В противном случае асинхронно ждет появления доступных для чтения данных и возвращает их.

    У данного метода в абстрактном классе есть реализация, которая основана на методах TryRead и WaitToReadAsync. Если опустить все инфраструктурные нюансы (исключения и cancelation tokens), то логика примерно такая попытаться прочитать объект с помощью TryRead. Если не удалось, то в цикле while(true) проверять результат метода WaitToReadAsync. Если true, то есть данные есть, вызвать TryRead. Если TryRead получается прочитать, то вернуть результат, в противном случае цикл по новой. Цикл нужен для неудачных попыток чтения в результате гонки потоков, сразу много потоков могут получить завершение WaitToReadAsync, но объект будет только один, соответственно только один поток сможет прочитать, а остальные уйдут на повторный круг.
    Однако данная реализация, как правило, переопределена на что-то более завязанное на внутреннем устройстве.


ChannelWriter производитель


Все аналогично потребителю, так что сразу смотрим методы:

  1. Виртуальный метод bool TryComplete(Exception? error = null)
    Пытается пометить канал как завершенный, т.е. показать, что в него больше не будет записано данных. В качестве необязательного параметра можно передать исключение, которое вызвало завершение канала. Возвращает true, если удалось завершить, в противном случае false (если канал уже был завершен или не поддерживает завршение);
  2. Абстрактный метод bool TryWrite(T item)
    Пытается записать в канал значение. Возвращает true, если удалось и false, если нет
  3. Абстрактный метод ValueTask<bool> WaitToWriteAsync(CancellationToken cancellationToken = default)
    Возвращает ValueTask со значением true, который завершится, когда в канале появится место для записи. Значение false будет в том случае, если записи в канал более не будут разрешены;
  4. Виртуальный метод ValueTask WriteAsync(T item, CancellationToken cancellationToken = default)
    Асинхронно пишет в канал. Например, в случае, если канал заполнен, операция будет реально асинхронной и завершится только после освобождения места под данную запись;
  5. Метод void Complete(Exception? error = null)
    Просто пытается пометить канал как завершенный с помощью TryComplete, а в случае неудачи кидает исключение.

Небольшой пример вышеописанного (для легкого начала ваших собственных экспериментов):

Channel<int> unboundedChannel = Channel.CreateUnbounded<int>();//Объекты ниже можно отправить в разные потоки, которые будут использовать их независимо в своих целяхChannelWriter<int> writer = unboundedChannel;ChannelReader<int> reader = unboundedChannel;//Первый поток может писать в каналint objectToWriteInChannel = 555;await writer.WriteAsync(objectToWriteInChannel);//И завершить его, при исключении или в случае, когда записал все, что хотелwriter.Complete();//Второй может читать данные из канала по мере их доступностиint valueFromChannel = await reader.ReadAsync();

А теперь перейдем к самой интересной части.

Асинхронность без алллокаций


В процессе написания и изучения кода, я осознал, что почти ничего интересного в реализации всех этих операций нет. В общем можно описать так избегание лишних блокировок с помощью конкурентных коллекций и обильное использование ValueTask, который является структурой, что экономит память. Однако спешу напомнить, что не стоит быстрой заменой проходиться по всем файлам на вашей ПЭВМ и заменять все Task на ValueTask. Он имеет смысл только в случаях, когда операция в большинстве случаев завершается синхронно. Ведь, как мы помним, при асинхронности вполне вероятна смена потока, а значит и стек уже будет не тот, что прежде. Да и вообще, истинный профессионал в области производительности знает не оптимизируй до возникновения проблем.

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

Интерфейс IValueTaskSource


Начнем наш путь с истоков структуры ValueTask, которая была добавлена в .net core 2.0 и дополнена в 2.1. Внутри этой структуры скрывается хитрое поле object _obj. Несложно догадаться, опираясь на говорящее название, что в этом поле может скрываться одна из 3 вещей null, Task/Task<T> или IValueTaskSource. На самом деле, это вытекает из способов создания ValueTask.

Как заверяет производитель, данную структуру следует использовать лишь очевидно с ключевым словом await. То есть не следует применять await много раз к одному и тому же ValueTask, использовать комбинаторы, добавлять несколько продолжений и тп. Также не следует получать результат из ValueTask более одного раза. А связано это как раз с тем, что мы пытаемся понять переиспользованием всего этого добра без выделения памяти.

Я уже упомянул интерфейс IValueTaskSource. Именно он помогает сэкономить память. Делается это с помощью переиспользования самого IValueTaskSource несколько раз для множества задач. Но именно из-за этого переиспользования и нет возможности баловаться с ValueTask.

Итак, IValueTaskSource. Данный интерфейс имеет 3 метода, реализовав которые вы будете успешно экономить память и время на выделении тех заветных байт.

  1. GetResult Вызывается единожды, когда в стейт машине, образованной на рантайме для асинхронных методов, понадобится результат. В ValueTask есть метод GetResult, который и вызывает одноименный метод интерфейса, который, как мы помним, может хранится в поле _obj.
  2. GetStatus Вызывается стейт машиной для определения состояния операции. Также через ValueTask.
  3. OnCompleted Опять же, вызывается стейт машиной для добавления продолжения к невыполненной на тот момент задаче.

Но несмотря на простой интерфейс, реализация потребует определенной сноровки. И тут можно вспомнить про то, с чего мы начали Channels. В данной реализации используется класс AsyncOperation, который является реализацией IValueTaskSource. Данный класс скрыт за модификатором доступа internal. Но это не мешает разобраться, в основных механизмах. Напрашивается вопрос, почему не дать реализацию IValueTaskSource в массы? Первая причина (хохмы ради) когда в руках молоток, повсюду гвозди, когда в руках реализация IValueTaskSource, повсюду неграмотная работа с памятью. Вторая причина (более правдоподобная) в то время, как интерфейс прост и универсален, реальная реализация оптимальна при использований определенных нюансов применения. И вероятно именно по этой причине можно найти реализации в самых разных частях великого и могучего .net, как то AsyncOperation под капотом каналов, AsyncIOOperation внутри нового API сокетов и тд.

CompareExchange


Довольно популярный метод популярного класса, позволяющий избежать накладных расходов на классические примитивы синхронизации. Думаю, большинство знакомы с ним, но все же стоит описать в 3 словах, ведь данная конструкция используется довольно часто в AsyncOperation.
В массовой литературе данную функцию называют compare and swap (CAS). В .net она доступна в классе Interlocked.

Сигнатура следующая:

public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class;

Имеются также перегрузи с int, long, float, double, IntPtr, object.

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

Допустим, вы хотите инкрементировать переменную, если ее значение меньше 10.

Далее идут 2 потока.

Поток 1 Поток 2
Проверяет значение переменной на некоторое условие (то есть меньше ли оно 10), которое срабатывает -
Между проверкой и изменением значения Присваивает переменной значение, не удовлетворяющее условию (например, 15)
Изменяет значение, хотя не должен, ведь условие уже не соблюдается -


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

location1 переменная, значение которой мы хотим поменять. Оно сравнивается с comparand, в случае равенства в location1 записывается value. Если операция удалась, то метод вернет прошлое значение переменной location1. Если же нет, то будет возращено актуальное значение location1.
Если говорить чуть глубже, то существует инструкция языка ассемблера cmpxchg, которая выполняет эти действия. Именно она и используется под капотом.

Stack dive


Рассматривая весь этот код я не раз наткнулся на упоминания Stack Dive. Это очень крутая и интересная штука, которая на самом деле очень нежелательна. Суть в том, что при синхронном выполнении продолжений мы можем исчерпать ресурсы стека.

Допустим, мы имеем 10000 задач, в стиле

//code1await ...//code2

Допустим, первая задача завершает выполнение и этим освобождает продолжение второй, которое мы начинаем тут же выполнять синхронно в этом потоке, то есть забирая кусок стека стек фреймом данного продолжения. В свою очередь, данное продолжение разблокирует продолжение третей задачи, которое мы тоже начинаем сразу выполнять. И так далее. Если в продолжении больше нет await'ов или чего-то, что как-то сбросит стек, то мы просто будем потреблять стековое пространство до упора. Что может вызвать StackOverflow и крах приложения. В рассмотрении кода я упомяну, как с этим борется AsyncOperation.

AsyncOperation как реализация IValueTaskSource


Source code.

Внутри AsyncOperation есть поле _continuation типа Action<object>. Поле используется для, не поверите, продолжений. Но, как это часто бывает в слишком современном коде, у полей появляются дополнительные обязанности (как сборщик мусора и последний бит в ссылке на таблицу методов). Поле _continuation из той же серии. Есть 2 специальных значения, которые могут хранится в этом поле, кроме самого продолжения и null. s_availableSentinel и s_completedSentinel. Данные поля показывают, что операция доступна и завершена соответственно. Доступна она бывает как раз для переиспользования для совершенно асинхронной операции.

Также AsyncOperation реализует IThreadPoolWorkItem с единственным методом void Execute() => SetCompletionAndInvokeContinuation(). Метод SetCompletionAndInvokeContinuation как раз и занимается выполнением продолжения. И данный метод вызывается либо напрямую в коде AsyncOperation, либо через упомянутый Execute. Ведь типы реализующие IThreadPoolWorkItem можно забрасывать в тред пул как-то вот так ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false).

Метод Execute будет выполнен тред пулом.

Само выполнение продолжения довольно тривиально.

Продолжение _continuation копируется в локальную переменную, на ее место записывается s_completedSentinel искусственный объект-марионетка (иль часовой, не знаю, как глаголить мне в нашей речи), который указывает, что задача завершена. Ну а далее локальная копия реального продолжения просто выполняется. При наличии ExecutionContext, данные действия постятся в контекст. Никакого секрета тут нет. Этот код может быть вызван как напрямую классом просто вызвав метод, инкапсулирующий эти действия, так и через интерфейс IThreadPoolWorkItem в тред пуле. Теперь можно догадаться, как работает функция с выполнением продолжений синхронно.

Первый метод интерфейса IValueTaskSource GetResult (github).

Все просто, он:

  1. Инкрементирует _currentId.
    _currentId то, что идентифицирует конкретную операцию. После инкремента она уже не будет ассоциирована с этой операцией. Поэтому не следует получать результат дважды и тп;
  2. помещает в _continuation делегат-марионетку s_availableSentinel. Как было упомянуто, это показывает, что этот экземпляр AsyncOperation можно испоьзовать повторно и не выделять лишней памяти. Делается это не всегда, а лишь если это было разрешено в конструкторе (pooled = true);
  3. Возвращает поле _result.
    Поле _result просто устанавливается в методе TrySetResult который описан ниже.

Метод TrySetResult (github).

Метод тривиален. он сохраняет принятый параметр в _result и сигнализирует о завершении, а именно вызывает метод SignalCompleteion, который довольно интересен.

Метод SignalCompletion (github).

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

В самом начале, если _comtinuation == null, мы записываем марионетку s_completedSentinel.

Далее метод можно разделить на 4 блока. Сразу скажу для простоты понимания схемы, 4 блок просто синхронное выполнение продолжения. То есть тривиальное выполнение продолжения через метод, как я описано в абзаце про IThreadPoolWorkItem.

  1. Если _schedulingContext == null, т.е. нет захваченного контекста (это первый if).
    Далее необходимо проверить _runContinuationsAsynchronously == true, то есть явно указано, что продолжения нужно выполнять как все привыкли асинхронно (вложенный if).
    При соблюдении данный условий в бой идет схема с IThreadPoolWorkItem описанная выше. То есть AsyncOperation добавляется в очередь на выполнение потоком тред пула. И выходим из метода.
    Следует обратить внимание, что если первый if прошел (что будет очень часто, особенно в коре), а второй нет, то мы не попадем в 2 или 3 блок, а спустимся сразу на синхронное выполнение продолжения т.е. 4 блок;
  2. Если _schedulingContext is SynchronizationContext, то есть захвачен контекст синхронизации (это первый if).
    По аналогии мы проверяем _runContinuationsAsynchronously = true. Но этого не достаточно. Необходимо еще проверить, контекст потока, на котором мы сейчас находимся. Если он отличен от захваченного, то мы тоже не можем просто выполнить продолжение. Поэтому если одно из этих 2 условий выполнено, мы отправляем продолжение в контекст знакомым способом:
    sc.Post(s => ((AsyncOperation<TResult>)s).SetCompletionAndInvokeContinuation(), this);
    

    И выходим из метода. опять же, если первая проверка прошла, а остальные нет (то есть мы сейчас находимся на том же контексте, что и был захвачен), мы попадем сразу на 4 блок синхронное выполнение продолжения;
  3. Выполняется, если мы не зашли в первые 2 блока. Но стоит расшифровать это условие.
    Хитрость в том, что _schedulingContext может быть на самом деле захваченным TaskScheduler, а не непосредственно контекстом. В этом случае мы поступаем также, как и в блоке 2, т.е. проверяем флаг _runContinuationsAsynchronously = true и TaskScheduler текущего потока. Если планировщик не совпадает или флаг не тот, то мы сетапим продолжение через Task.Factory.StartNew и передаем туда этот планировщик. И выходим из метода.
  4. Как и сказал в начале просто выполняем продолжение на текущем потоке. Раз мы до сюда дошли, то все условия для этого соблюдены.

Второй метод интерфейса IValueTaskSource GetStatus (github)
Просто как питерская пышка.

Если _continuation != _completedSentinel, то возвращаем ValueTaskSourceStatus.Pending
Если error == null, то возвращаем ValueTaskSourceStatus.Succeeded
Если _error.SourceException is OperationCanceledException, то возвращаем ValueTaskSourceStatus.Canceled
Ну а коль уж до сюда дошли, то возвращаем ValueTaskSourceStatus.Faulted

Третий и последний, но самый сложный метод интерфейса IValueTaskSource OnCompleted (github)

Метод добавляет продолжение, которое выполняется по завершению.

При необходимости захватывает ExecutionContext и SynchronizationContext.

Далее используется Interlocked.CompareExchange, описанный выше, чтобы сохранить продолжение в поле, сравнивая его с null. Напоминаю, что CompareExchange возвращает актуальное значение переменной.

Если сохранение продолжения прошло, то возвращается значение, которое было в переменной до обновления, то есть null. Это означает, что операция еще не завершилась на момент записи продолжения. И тот, кто ее завершит сам со всем разберется (как мы смотрели выше). И нам нет смысла выполнять какие-то дополнительные действия. И на этом работа метода завершается.

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

Таким образом проверяем возвращенное значение, равно ли оно s_completedSentinel именно оно было бы записано в случае завершения.

  • Если это не s_completedSentinel, то нас использовали не по плану попытались добавить более одного продолжения. То есть то, которое уже записано, и то, которое пишем мы. А это исключительная ситуация;
  • Если это s_completedSentinel, то это один из допустимых исходов, операция уже завершена и продолжение должны вызвать мы, здесь и сейчас. И оно будет выполнено асинхронно в любом случае, даже если _runContinuationsAsynchronously = false.
    Сделано это так, потому что если мы дошли до этого места, значит мы внутри метода OnCompleted, внутри awaiter'а. А синхронное выполнение продолжений именно здесь грозит упомянутым стек дайвом. Сейчас вспомним, для чего нам нужна эта AsyncOperation System.Threading.Channels. А там ситуация может быть очень легко достигнута, если о ней не задуматься. Допустим, мы читатель в ограниченном канале. Мы читаем элемент и разблокируем писателя, выполняем его продолжение синхронно, что разблокирует очередного читателя(если читатель очень быстр или их несколько) и так далее. Тут стоит осознать тонкий момент, что именно внутри awaiter'а возможна эта ситуация, в других случаях продолжение выполнится и завершится, что освободит занятый стек фрейм. А постоянный зацеп новых продолжений вглубь стека порождается постоянным выполнением продолжения внутри awaiter'а.
    В целях избежания данной ситуации, несмотря ни на что необходимо запустить продолжение асинхронно. Выполняется по тем же схемам, что и первые 3 блока в методе SignalCompleteion просто в пуле, на контексте или через фабрику и планировщик

А вот и пример синхронных продолжений:

class Program    {        static async Task Main(string[] args)        {            Channel<int> unboundedChannel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions            {                AllowSynchronousContinuations = true            });            ChannelWriter<int> writer = unboundedChannel;            ChannelReader<int> reader = unboundedChannel;            Console.WriteLine($"Main, before await. Thread id: {Thread.CurrentThread.ManagedThreadId}");            var writerTask = Task.Run(async () =>            {                Thread.Sleep(500);                int objectToWriteInChannel = 555;                Console.WriteLine($"Created thread for writing with delay, before await write. Thread id: {Thread.CurrentThread.ManagedThreadId}");                await writer.WriteAsync(objectToWriteInChannel);                Console.WriteLine($"Created thread for writing with delay, after await write. Thread id: {Thread.CurrentThread.ManagedThreadId}");            });            //Blocked here because there are no items in channel            int valueFromChannel = await reader.ReadAsync();            Console.WriteLine($"Main, after await (will be processed by created thread for writing). Thread id: {Thread.CurrentThread.ManagedThreadId}");            await writerTask;            Console.Read();        }    }

Output:

Main, before await. Thread id: 1
Created thread for writing with delay, before await write. Thread id: 4
Main, after await (will be processed by created thread for writing). Thread id: 4
Created thread for writing with delay, after await write. Thread id: 4
Подробнее..

Из песочницы IOptions и его друзья

20.06.2020 00:17:13 | Автор: admin

Во время разработки часто возникает потребность для вынесения параметров в конфигурационные файлы. Да и вообще хранить разные конфигурационный константы в коде является признаком дурного тона. Один из вариантов хранения настроек использования конфигурационных файлов. .Net Core из коробки умеет работать с такими форматами как: json, ini, xml и другие. Так же есть возможность писать свои провайдеры конфигураций. (Кстати говоря за работу с конфигурациями отвечает сервис IConfiguration и IConfigurationProvider для доступа к конфигурациям определенного формата и для написания своих провайдеров)


image


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


На MSDN есть статья, которая должна раскрывать все вопросы. Но, как всегда, не все так просто.


IOptions


Does not support:
Reading of configuration data after the app has started.
Named options

Is registered as a Singleton and can be injected into any service lifetime.

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


Но у меня большие претензии к Microsoft в плане нейминга. На мой взгляд, если человек недостаточно знаком с технологией, или вообще видит ее в первый раз, то данный интерфейс это то, что первое придет на ум для использования. И потом, вероятно далеко не сразу, человек выяснит, что то все работает совсем не так, как он задумывал.


Я привык, что довольно большая часть конфигураций в моих проектах может довольно часто меняться. И лично мне хотелось бы, что бы интерфейс, который буквально кричит о том, что его надо использовать для конфигурации вел себя более очевидно. (Хотя конечно я могу быть не прав, и это только мои придирки. А большая часть людей использует файлы конфигураций по-другому)


IOptionsSnapshot


Is useful in scenarios where options should be recomputed on every request

Is registered as Scoped and therefore cannot be injected into a Singleton service.

Supports named options

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


MSDN нам говорит, что не может быть заинжекчен в Singletone на самом деле может (это прям тема для отдельного поста), но тогда и сам он начинает себя вести как Singletone.


IOptionsMonitor


Is used to retrieve options and manage options notifications for TOptions instances.

Is registered as a Singleton and can be injected into any service lifetime.

Supports:
Change notifications
Named options
Reloadable configuration
Selective options invalidation (IOptionsMonitorCache)

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


IOptionsMonitorCache интерфейс для построения обычного кэша на базе IOptionsMonitor.


Практика


Все тесты проводились на следующем окружении


sw_versProductName:    Mac OS XProductVersion: 10.15.5BuildVersion:   19F101dotnet --version3.1.301

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


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


В качестве примера будет простое Web API


 public class Program {     public static void Main(string[] args)     {         CreateHostBuilder(args).Build().Run();     }     public static IHostBuilder CreateHostBuilder(string[] args) =>         Host.CreateDefaultBuilder(args)             .ConfigureWebHostDefaults(webBuilder =>             {                 webBuilder.UseKestrel();                 webBuilder.UseStartup<Startup>();                 webBuilder.UseUrls("http://*:5010/");             })             .UseDefaultServiceProvider(options => options.ValidateScopes = false); }

Клиент, который будет к нему обращаться


 static async Task Main(string[] args) {     using var client = new HttpClient();     var prevResponse = String.Empty;     while (true)     {         var response = await client.GetStringAsync("http://localhost:5010/settings");         if (response != prevResponse) // пишем в консоль только, если настройки изменились         {             Console.WriteLine(response);             prevResponse = response;         }     } }

В Web API создаем 3 сервиса, который принимает все 3 варианта конфигураций в конструктор и возвращают текущее значение.


private readonly IOptions<TestGroupSettings> _testOptions;private readonly IOptionsSnapshot<TestGroupSettings> _testOptionsSnapshot;private readonly IOptionsMonitor<TestGroupSettings> _testOptionsMonitor;public ScopedService(IOptions<TestGroupSettings> testOptions, IOptionsSnapshot<TestGroupSettings> testOptionsSnapshot,    IOptionsMonitor<TestGroupSettings> testOptionsMonitor){    _testOptions = testOptions;    _testOptionsSnapshot = testOptionsSnapshot;    _testOptionsMonitor = testOptionsMonitor;}

Сервисы будут 3х скоупов: Singletone, Scoped и Transient.


public void ConfigureServices(IServiceCollection services){    services.Configure<TestGroupSettings>(Configuration.GetSection("TestGroup"));    services.AddSingleton<ISingletonService, SingletonService>();    services.AddScoped<IScopedService, ScopedService>();    services.AddTransient<ITransientService, TransientService>();    services.AddControllers();}

В процессе работы нашего Web Api изменяем значение TestGroup.Test файла appsettings.json


Имеем следующую картину:
Сразу после запуска


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0SingletonService IOptionsMonitor value: 0ScopedService IOptions value: 0ScopedService IOptionsSnapshot value: 0ScopedService IOptionsMonitor value: 0TransientService IOptions value: 0TransientService IOptionsSnapshot value: 0TransientService IOptionsMonitor value: 0

Изменяем нашу настройку и получаем интересную картину
Сразу после изменения


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: 0 // не измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: // стала пустойScopedService IOptionsMonitor value: 0 // не измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: // стала пустойTransientService IOptionsMonitor value: 0 // не изменилась

Следующий вывод в консоль (конфиг больше не менялся)


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: 0 // не измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: changed setting // измениласьScopedService IOptionsMonitor value: 0 // не измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: changed setting // измениласьTransientService IOptionsMonitor value: 0 // не изменилась

Последний вывод (конфиг также не менялся)


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: changed setting // измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: changed setting // измениласьScopedService IOptionsMonitor value: changed setting // измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: changed setting // измениласьTransientService IOptionsMonitor value: changed setting // изменилась

Что имеем в итоге? А имеем то, что IOptionsMonitor не такой шустрый, как нам говорит документация. Как можно заметить IOptionsSnapshot может вернуть пустое значение. Но, он работает быстрее, чем IOptionsMonitor.


Пока не особо понятно откуда берется это пустое значение. И самое интересное, что подобное поведение проявляется не всегда. Как-то через раз в моем примере IOptionsMonitor и IOptionsSnapshot отрабатывают одновременно.


Выводы


Если вам нужно передавать конфигурацию, которые никогда в процессе жизни вашего приложения не будут меняться используете IOptions.


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


Если же вам нужны наиболее актуальные значения (или почти) используйте IOptionsMonitor.


Буду рад, если вы запустите пример у себя, и расскажете, повторяется подобное поведение или нет. Возможно мы имеем баг на MacOS, а может это by design.


Продолжу разбираться с этой темой, а пока завел issue, может там прояснят такое поведение.

Подробнее..

Xamarin.Forms. Личный опыт использования

25.06.2020 14:15:14 | Автор: admin
В статье речь пойдет о Xamarin.Forms на примере живого проекта. Кратко поговорим о том, что такое Xamarin.Forms, сравним с похожей технологией WPF, увидим, как достигается кроссплатформенность. Также разберём узкие места, с которыми мы столкнулись в процессе разработки, и добавим немного реактивного программирования с ReactiveUI.
image

Кроссплатформа что выбрать?


В современном мире, в век огромного количества различных устройств и операционных систем, более конкурентноспособными являются приложения, способные работать сразу на нескольких платформах. Для реализации кроссплатформенности существуют разные подходы: написание нативного кода для каждой целевой платформы (по сути, нескольких различных приложений) или использование специальных фреймворков, позволяющих разрабатывать единый код для всех случаев. Есть еще кроссплатформенные языки программирования или же среды исполнения, например, Java Virtual Machine, но здесь я их касаться не буду.

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

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

А теперь к сути


Не так давно нашей команде пришлось лицом к лицу столкнуться с кроссплатформенной разработкой. Задача от заказчика звучала так:
  • Создать iOS-приложение, работающее на iPad;
  • Разработать такое же приложение с расширенным функционалом под Windows 10;
  • Всё это при условии, что правки в дальнейшем могут вноситься как в оба приложения одновременно, так и по отдельности;
  • Сделать приложение максимально гибким, поддерживаемым и расширяемым, потому что техническое задание, как обычно, менялось со скоростью света.

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

Xamarin это инструмент для создания приложений на языках семейства .NET (C#, F#, Visual Basic), который позволяет создавать единый код, работающий на Android, iOS и Windows (UWP-приложения). Это xaml-подобная технология, то есть интерфейс описывается декларативно в формате xml, вы сразу видите, как элементы расположены на форме и какие свойства имеют. Такой подход очень удобен, в отличие, например, от Windows.Forms, в котором, если бы не графический редактор, разрабатывать и редактировать пользовательские интерфейсы было бы крайне сложно, так как все элементы и их свойства создаются динамически. У меня был опыт разработки подобных интерфейсов без декларативных описаний в среде, не имеющей удобного графического редактора, и я не хочу его повторять. В Xamarin.Forms сохранена возможность динамического создания элементов интерфейса в программном коде, но для чистоты кармы и благодарности от последователей вашего кода всё, что можно описать декларативно, лучше так и описывать.

Xamarin.Forms и WPF


Еще одной известной xaml-подобной технологией является WPF (Windows Presentation Foundation). Это потрясающий инструмент для создания красивых интерактивных пользовательских интерфейсов. На мой взгляд, это один из лучших инструментов, созданных Microsoft, имеющий, правда, серьезный недостаток такие приложения можно разрабатывать только под Windows.

Xamarin.Forms включает в себя урезанную версию WPF, агрегируя лишь те компоненты, которые имеют аналоги на других платформах. С одной стороны, это даёт разработчикам, имеющим опыт работы с WPF, неплохую фору для входа в Xamarin.Forms. С другой же, познав всю прелесть и возможности WPF, программисту будет трудно то тут, то там натыкаться на отсутствие всех этих преимуществ в Xamarin.Forms. Мне было крайне грустно осознать, что я не могу использовать шаблоны для элементов управления, которыми так привыкла жонглировать в WPF, в Xamarin.Forms их попросту нет. Или удобный и мощный ItemControl компонент WPF, позволяющий разместить на форме список элементов, задавая каким угодно образом их внешний вид и расположение. С помощью одного только этого компонента как основного можно написать, например, простенький прототип игры в бильярд. В Xamarin.Forms существуют аналоги ItemControl, но все они имеют конкретную реализацию внешнего вида списка, что лишает их необходимой гибкости и вынуждает разработчиков идти на GitHub за велосипедами.

Кроссплатформенность в Xamarin.Forms


Однако не бывает худа без добра. Своими ограничениями Xamarin.Forms платит за кроссплатформенность, которая действительно легко и удобно достигается средствами этого инструмента. Вы пишете общий код для iOS, Android и Windows, а Xamarin сам разбирается, как связать ваш код с родным для каждой платформы API. Кроме того, есть возможность писать не только общий, но и платформозависимый код, и Xamarin тоже поймет, что и где вызывать. Одним из главных механизмов достижения кроссплатформенности является наличие умных сервисов, способных осуществлять кросс-зависимые вызовы, то есть обращаться к той или иной реализации определённого функционала в зависимости от платформы. Платформозависимый код можно писать не только на C#, но и добавлять его в xaml-разметку. В нашем случае iOS версия была урезанной и часть графического интерфейса нужно было скрыть, размеры некоторых элементов также зависели от платформы.

Для большей наглядности приведу небольшой пример. В нашем проекте мы использовали библиотеку классов, предоставленную заказчиком, которая была написана на C++. Назовём её MedicalLib. MedicalLib собиралась в две разные сборки в зависимости от платформы (статическая MedicalLib.a для iOS и динамическая MedicalLib.dll для Windows). Кроме того, она имела различные wrapper-классы (классы-переходники) для вызова неуправляемого кода. Основной проект (общая кодовая база) содержал описание API MedicalLib (интерфейс), а платформозависимые проекты для iOS и Windows конкретные реализации этого интерфейса и ссылки на сборки MedicalLib.a и MedicalLib.dll соответственно. Из основного кода мы вызывали те или иные функции абстракции, не задумываясь, как именно они реализованы, а механизмы Xamarin.Forms, понимая, на какой платформе запущено приложение, вызывали необходимую реализацию и подгружали конкретную сборку.

Примерно так это можно изобразить схематично:
image

Среда разработки


Для написания программ с Xamarin.Forms используется Visual Studio. На MacOS Visual Studio for Mac. Многие разработчики считают это больше недостатком, ссылаясь на тяжеловесность этой IDE. Для меня Visual Studio является привычной средой разработки и на высокопроизводительных компьютерах не доставляет каких-либо неудобств. Хотя Mac-версия этой IDE пока еще далека от идеала и имеет на порядок больше недочетов, чем её Windows-собрат. Для мобильной разработки имеется целый ряд встроенных эмуляторов мобильных устройств, а также возможность подключить реальный девайс для отладки.

Reactive UI и реактивная модель


В основу любого проекта Xamarin.Forms отлично ложится паттерн проектирования MVVM (Model View View Model), главным принципом которого является отделение внешнего вида пользовательского интерфейса от бизнес-логики. В нашем случае мы использовали MVVM, что действительно оказалось удобно. Важным моментом реализации MVVM является механизм оповещений View о том, что какие-то данные изменились и это необходимо отобразить на интерфейсе. Думаю, многие разработчики на WPF слышали об интерфейсе INotifyPropertyChanged, реализуя который во вью-моделях, мы получаем возможность оповещать интерфейс об изменениях. Этот способ имеет свои плюсы и минусы, но главным недостатком является запутанность и громоздкость кода в случаях, когда во вью-моделях есть вычисляемые свойства (например, Name, Surname и вычисляемое FullName). Мы выбрали более удобный фреймворк ReactiveUI. Он уже содержит реализацию INotifyPropertyChanged, а также много других преимуществ например, IObservable.

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

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

Реализовано это было следующим образом: на бэкэнде постоянно крутился некий генератор данных, которые поступали в несколько IObservable. На каждый IObservable во вью-моделях были подписаны соответствующие свойства, которые с помощью механизмов ReactiveUI выводили данные на интерфейс в нужном виде.
image
Взаимодействие с интерфейсом в обратном направлении (т.е. обработка действий пользователя), было реализовано с помощью так называемых Interaction взаимодействий, которые инициировались при работе пользователя с UI (например, при нажатии на кнопку), а обрабатывались в любом месте приложения. Что-то наподобие интерфейса ICommand и команд в WPF, только интеракции в нашем случае назначались на интерфейсные элементы не декларативно (как с командами в WPF), а программно, что показалось не очень удобным.

Выше я уже проводила аналогию с WPF. Для наглядности покажу, как выглядит наша архитектура в сравнении со стандартным WPF-приложением:
image
Весь набор инструментов, описанных на этой схеме, мы получили, используя Reactive UI.

Сложности в процессе разработки


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

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

При сворачивании окна приложение переходит в состояние Suspended, и Windows выделяет ему меньше ресурсов. Это было критично, так как в одной из вариаций наш Data Generator работал, интегрируясь с внешними источниками и получая данные через сокеты. При сворачивании окна сокеты продолжали получать данные, заполняя свой внутренний буфер пакетами. А при выходе из режима Suspended все эти пакеты тут же помещались в буфер приложения, начиная обрабатываться только в этот момент. Из-за этого происходила задержка в отображении данных после развертывания окна, которая была пропорциональна времени, проведенному в свёрнутом виде. Всё решилось грамотной обработкой событий Suspending и Resuming, при наступлении которых мы сохраняли состояние приложения и закрывали сокеты, открывая их снова при восстановлении штатного режима работы.

Ещё одной непростой задачей стало добавление второго окна приложения (это было необходимо в рамках технического задания). Сейчас я не могу сказать, были ли это ограничения UWP, Xamarin.Forms или же наша собственная ошибка краш происходил в конструкторе одного из представлений из-за UI-потоков. Позже выяснилось, что библиотека, предоставленная заказчиком и являющаяся ядром обработки данных, однопользовательская и хранит в себе состояние. Таким образом, два независимых окна требовали двух независимых экземпляров библиотеки, что добавило нам одну большую низкоуровневую задачу, решение которой не было бы тривиальным. Мы решили, что овчинка выделки не стоит и проще не открывать отдельное окно, а создавать новый экземпляр приложения со своими настройками и адресным пространством. В итоге со стороны пользователя это выглядело в точности как второе окно (даже при закрытии главного окна второе тоже завершало свою деятельность), и разницу можно было заметить, только заглянув в диспетчер задач и увидев второй процесс.

Ну, и главный недостаток UWP, с которым пришло немало повозиться, это политика его распространения. Чтобы установить приложение, его либо необходимо загрузить в Microsoft Store и скачивать оттуда, либо поставлять дистрибутив другим способом с одним ограничением тогда при его установке необходимо дать операционной системе соответствующие разрешения (Sideloaded Apps в настройках Windows). Техническое задание требовало реализации второго подхода, однако политика безопасности заказчика запрещала сотрудникам компании менять подобные настройки. В итоге нам пришлось написать инсталлер, который перед установкой включал Sideloaded Apps, устанавливал пакет и в конце выключал эту настройку (естественно, по согласованию с заказчиком).

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

Что касается архитектуры и каких ошибок можно было бы избежать?

Выше я уже писала об использовании для взаимодействия с пользователем Interaction от ReactiveUI. Такие интеракции можно обработать в любом месте приложения, что является не только их достоинством, но и недостатком, так как может внести большую путаницу. Чтобы понять, почему при нажатии на кнопку она окрашивается в красный цвет, а не в зелёный, вам нужно найти все обработчики интеракции и проверить код в них. Договоренность внутри команды обрабатывать интеракции унифицированно может помочь снизить негативное влияние этого фактора, но избежать его совсем вряд ли удастся.

Не меньшим по значимости недостатком описанных выше интеракций является то, что управлять порядком вызовов обработчиков практически невозможно. Мы наступили еще на одни грабли и использовали их не только для взаимодействия с пользователем, но и для общения вью-моделей друг с другом. Это часто играло против нас, так как из-за неконтролируемости вызовов обработчиков мы получали много плавающих багов. Поэтому использовать Interaction я рекомендую с большой осторожностью, а при реализации MVVM прорабатывать иные подходы к взаимодействию вью-моделей.

Еще стоит отметить, что достаточно много времени наша команда потратила на адаптацию визуальной части пользовательского интерфейса под Windows-приложение. Сжатые сроки и приоритеты заказчика поставили разработку iOS-версии на первое место, поэтому к UWP мы приступили, имея уже наполовину готовый проект, работающий на iOS. Изначально не предполагалось изменение разрешения, и приложение оказалось не готовым к свободному масштабированию окна, не имея достаточной гибкости в расположении элементов, что привело нас к довольно объемному рефакторингу всех визуальных компонентов. Поэтому не могу не отметить, что разработка кроссплатформенного проекта всё-таки должна вестись параллельно на всех предполагаемых платформах.

Итог


Xamarin.Forms часто считают сырой и недоработанной технологией. Не могу сказать, что наш проект столкнулся с какими-то критичными проблемами именно со стороны Xamarin. Опыт нашей команды подтверждает, что адаптация приложения под вторую платформу процесс достаточно простой и быстрый. Не всё прошло гладко и без проблем, и задачи по адаптации всё-таки были например, из-за различия работы файловых систем или визуального отображения некоторых стилей. Но все эти проблемы либо устранялись достаточно быстро, либо не имели весомого значения для заказчика.

Благодаря архитектуре и выбранным решениям, со всеми их достоинствами и недостатками, проект уже полтора года наращивает функционал без какого-либо глобального рефакторинга и чувствует себя твёрдо стоящим на ногах. Инструмент работает и со своими основными задачами справляется. Кроме того, он не является новым для IT-сообщества, поэтому документации, которая помогает разобраться в тонкостях, достаточно. То, что теперь кроссплатформенную разработку можно вести на платформе .NET и удобном С#, которые предпочитают многие программисты, является неоспоримым преимуществом и может стать финальным аккордом в решении использовать Xamarin.Forms и на вашем проекте.
Подробнее..

Как анализатор PVS-Studio стал находить ещё больше ошибок в проектах на Unity

29.06.2020 12:20:00 | Автор: admin
image1.png

Разрабатывая статический анализатор PVS-Studio, мы стараемся развивать его в различных направлениях. Так, наша команда работает над плагинами для IDE (Visual Studio, Rider), улучшением интеграции с CI и т. д. Увеличение эффективности анализа проектов под Unity также является одной из наших приоритетных целей. Мы считаем, что статический анализ позволит программистам, использующим этот игровой движок, повысить качество своего исходного кода и упростить работу над любыми проектами. Поэтому хотелось бы увеличить популярность PVS-Studio среди компаний, занимающихся разработкой под Unity. Одним из первых шагов в реализации данной задумки стало написание нами аннотаций для методов, определённых в движке. Это позволяет контролировать корректность кода, связанного с вызовами аннотируемых методов.

Введение


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

Например, вызов метода GetComponent выглядит несколько странным, если значение, возвращаемое им, не используется. Ерундовая ошибка? Отнюдь. Конечно, это может быть попросту лишний вызов, забытый и покинутый всеми. А может быть так, что пропущено какое-то важное присваивание. Аннотации могут помочь анализатору найти подобные и многие другие ошибки.

Конечно же, нами уже написано множество аннотаций для анализатора. Например, проаннотированы методы классов из пространства имён System. Кроме того, существует и механизм автоматического аннотирования некоторых методов. Подробнее об этом можно узнать здесь. Отмечу, что указанная статья больше рассказывает про часть PVS-Studio, отвечающую за анализ проектов на C++. Тем не менее, ощутимой разницы в принципе работы аннотаций для C# и C++ нет.

Написание аннотаций для методов Unity


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

Изначальная идея состояла в том, чтобы покрыть аннотациями вообще все методы Unity, однако, их оказалось очень много. Вследствие этого, мы решили для начала проаннотировать методы из наиболее часто используемых классов.

Сбор информации


Сначала необходимо было выяснить, какие же именно классы используются чаще других. Кроме того, важным аспектом является обеспечение возможности сбора результатов аннотирования новых ошибок, которые анализатор найдёт в реальных проектах благодаря написанным аннотациям. Поэтому первым шагом был поиск соответствующих open-source проектов. Однако сделать это оказалось не так уж просто.

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

Конечно, иногда везло. Например, жемчужиной в этой коллекции является MixedRealityToolkit. Кода в нём уже прилично, а значит, и собранная статистика использования Unity-методов в таком проекте будет более полной.

Таким образом, было набрано 20 проектов, использующих возможности движка. Для того, чтобы найти наиболее часто используемые классы, была написана основанная на Roslyn утилита, производящая подсчёт вызовов методов из Unity. Такую программу, кстати, тоже вполне можно назвать статическим анализатором. Ведь если задуматься, она действительно анализирует исходники, не прибегая к запуску самого проекта.

Написанный "анализатор" позволил найти классы, средняя частота использования которых в найденных проектах наиболее высока:

  • UnityEngine.Vector3
  • UnityEngine.Mathf
  • UnityEngine.Debug
  • UnityEngine.GameObject
  • UnityEngine.Material
  • UnityEditor.EditorGUILayout
  • UnityEngine.Component
  • UnityEngine.Object
  • UnityEngine.GUILayout
  • UnityEngine.Quaternion
  • И т.д.

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

Аннотирование


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

Во время проведения таких проверок были найдены интересные особенности работы некоторых методов. К примеру, запуск кода

MeshRenderer renderer = cube.GetComponent<MeshRenderer>();Material m = renderer.material;List<int> outNames = null;m.GetTexturePropertyNameIDs(outNames);

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

MeshRenderer renderer = cube.GetComponent<MeshRenderer>();Material m = renderer.material;string keyWord = null;bool isEnabled = m.IsKeywordEnabled(keyWord);

Указанные проблемы актуальны для редактора Unity 2019.3.10f1.

Сбор результатов


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

Процедура тестирования аннотаций выполняется в автоматическом режиме с помощью специально написанной под эти нужды программы CSharpAnalyserTester. Он производит запуск анализа на проектах, после чего сравнивает получаемые логи с эталонными и формирует файлы, содержащие информацию о различиях.

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

Как уже отмечалось ранее, с поиском больших открытых проектов под Unity возникли сложности. Это неприятно, ведь для них анализатор смог бы выдать больше интересных срабатываний. В то же время, было бы намного больше различий между эталонными логами и логами, сформированными после проведения аннотирования.

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

Например, был найден несколько странный вызов GetComponent:

void OnEnable(){  GameObject uiManager = GameObject.Find("UIRoot");  if (uiManager)  {    uiManager.GetComponent<UIManager>();  }}

Предупреждение анализатора: V3010 The return value of function 'GetComponent' is required to be utilized. ADDITIONAL IN CURRENT UIEditorWindow.cs 22

Опираясь на документацию, логично сделать вывод, что значение, возвращаемое этим методом, должно как-то использоваться. Поэтому при аннотировании он был отмечен соответствующим образом. Тут же результат вызова ничему не присваивается, что выглядит немного странно.

А вот ещё один пример дополнительных срабатываний анализатора:

public void ChangeLocalID(int newID){  if (this.LocalPlayer == null)                          // <=  {    this.DebugReturn(      DebugLevel.WARNING,       string.Format(        ....,         this.LocalPlayer,         this.CurrentRoom.Players == null,                // <=        newID        )    );  }  if (this.CurrentRoom == null)                          // <=  {    this.LocalPlayer.ChangeLocalID(newID);               // <=    this.LocalPlayer.RoomReference = null;  }  else  {    // remove old actorId from actor list    this.CurrentRoom.RemovePlayer(this.LocalPlayer);    // change to new actor/player ID    this.LocalPlayer.ChangeLocalID(newID);    // update the room's list with the new reference    this.CurrentRoom.StorePlayer(this.LocalPlayer);  }}

Предупреждения анализатора:

  • V3095 The 'this.CurrentRoom' object was used before it was verified against null. Check lines: 1709, 1712. ADDITIONAL IN CURRENT LoadBalancingClient.cs 1709
  • V3125 The 'this.LocalPlayer' object was used after it was verified against null. Check lines: 1715, 1707. ADDITIONAL IN CURRENT LoadBalancingClient.cs 1715

Отметим, что PVS-Studio не обращает внимания на передачу LocalPlayer в string.Format, так как это не приведёт к ошибке. Да и выглядит код так, будто написано это намеренно.

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

Дело в том, что в методе DebugReturn производится несколько вызовов, которые в теории могли бы повлиять на значение свойства CurrentRoom:

public virtual void DebugReturn(DebugLevel level, string message){  #if !SUPPORTED_UNITY  Debug.WriteLine(message);  #else  if (level == DebugLevel.ERROR)  {    Debug.LogError(message);  }  else if (level == DebugLevel.WARNING)  {    Debug.LogWarning(message);  }  else if (level == DebugLevel.INFO)  {    Debug.Log(message);  }  else if (level == DebugLevel.ALL)  {    Debug.Log(message);  }  #endif}

Анализатору неизвестны особенности работы вызываемых методов, а значит, и неизвестно, как они будут влиять на ситуацию. Так, PVS-Studio предполагает, что значение this.CurrentRoom могло измениться во время работы метода DebugReturn, поэтому далее и производится проверка.

Аннотации же дали информацию о том, что методы, вызываемые внутри DebugReturn, не повлияют на значения других переменных. Следовательно, использование переменной перед её проверкой на равенство null можно считать подозрительным.

Заключение


Подводя итог, стоит сказать, что аннотирование Unity-специфичных методов, вне всяких сомнений, позволит находить больше различных ошибок в проектах, использующих этот движок. Тем не менее, покрытие аннотациями всех доступных методов потребует достаточно много времени. Более эффективным будет аннотировать в первую очередь наиболее часто используемые. Однако, чтобы понять, какие именно классы используются чаще, нужны подходящие проекты с большой кодовой базой. Кроме того, крупные проекты позволяют гораздо лучше контролировать результативность аннотирования. Всем этим мы продолжим заниматься в ближайшее время.

Анализатор постоянно развивается и дорабатывается. Добавление аннотаций для методов Unity лишь один из примеров расширения его возможностей. Таким образом, со временем эффективность работы PVS-Studio растёт. Поэтому, если вы ещё не пробовали PVS-Studio, самое время исправить это, загрузив его с соответствующей страницы. Там же можно получить триальный ключ для анализатора, чтобы ознакомиться с его возможностями, проверив различные проекты.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Lipilin. How the PVS-Studio analyzer began to find even more errors in Unity projects.
Подробнее..

Из песочницы Пишем спецификацию под Nvidia Kepler (бинарники CUDA, версия языка sm_30) для Ghidra

02.07.2020 00:11:00 | Автор: admin
Для обычных процессорных языков уже написано довольно много спецификаций для Ghidra, однако для графических ничего нет. Оно и понятно, ведь там своя специфика: предикаты, константы, через которые передаются параметры в том числе, и другие вещи, унаследованные от шейдеров. Кроме того формат, который используется для хранения кода, зачастую проприетарный, и его нужно самостоятельно ревёрсить.

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

Первая программа простейший axpy (аналог hello world для GPGPU). Вторая помогает понять реализацию условий и прыжков на GPU, т.к. там всё по-другому.

Во всех Nvidia языках используется кодировка little endian, так что сразу копируем байты из hex-редактора в какой-нибудь блокнот (например, Notepad++) в обратном порядке по 8 штук (длина инструкций здесь постоянна). Затем через программерский калькулятор (подойдёт тот, что от Microsoft) переводим в двоичный код. Далее ищем совпадения, составляем маску инструкции, затем операндов. Для декодирования и поиска маски использовался редактор hex и дизассемблер cuobjdump, иногда требуется ассемблер, как в AMDGPU(т.к. там дизассемблер недоступен, но это тема для отдельной статьи). Работает это так: пробуем последовательно инвертировать все подозрительные биты в калькуляторе, затем получаем новое шестнадцатеричное значение для байтов, их подставляем в бинарник, скомпилированный через nvcc либо ассемблер, если он существует, что бывает не всегда. Затем через cuobjdump проверяем.

Выкладываю в формате исходник (в основном на C, без плюсов и ООП для более тесной связи с машинным GPU кодом), затем дизасм + сразу байты, ибо так удобнее, их как раз не надо менять местами.

Копируем в axpy.cu и скомпилируем через cmd: nvcc axpy.cu --cubin --gpu-architecture sm_30
Полученный ELF-файл с именем axpy.cubin дизассемблируем там же: cuobjdump axpy.cubin -sass

Пример 1:

__global__ void axpy(float param_1, float* param_2, float* param_3) {unsigned int uVar1 = threadIdx.x;param_2[uVar1] = param_1 * param_3[uVar1];}

Дамп
/*0000*//* 0x22c04282c2804307 *//*0008*/ MOV R1, c[0x0][0x44];/* 0x2800400110005de4 *//*0010*/ S2R R0, SR_TID.X;/* 0x2c00000084001c04 *//*0018*/ MOV32I R5, 0x4;/* 0x1800000010015de2 *//*0020*/ ISCADD R2.CC, R0, c[0x0][0x150], 0x2;/* 0x4001400540009c43 *//*0030*/ LD.E R2, [R2];/* 0x8400000000209c85 *//*0038*/ ISCADD R4.CC, R0, c[0x0][0x148], 0x2;/* 0x4001400520011c43 *//*0040*//* 0x20000002e04283f7 *//*0048*/ IMAD.U32.U32.HI.X R5, R0, R5, c[0x0][0x14c];/* 0x208a800530015c43 *//*0050*/ FMUL R0, R2, c[0x0][0x140];/* 0x5800400500201c00 *//*0058*/ ST.E [R4], R0;/* 0x9400000000401c85 *//*0060*/ EXIT;/* 0x8000000000001de7 *//*0068*/ BRA 0x68;/* 0x4003ffffe0001de7 *//*0070*/ NOP;/* 0x4000000000001de4 *//*0078*/ NOP;/* 0x4000000000001de4 */


Результат декомпиляции
void axpy(float param_1,float *param_2,float *param_3) {  uint uVar1;    uVar1 = *&threadIdx.x;  param_2[uVar1] = param_3[uVar1] * param_1;  return;}


Пример 2:

__global__ void predicates(float* param_1, float* param_2) {    unsigned int uVar1 = threadIdx.x + blockIdx.x * blockDim.x;    if ((uVar1 > 5) & (uVar1 < 10)) param_1[uVar1] = uVar1;    else param_2[uVar1] = uVar1;}

Дамп
/*0000*//* 0x2272028042823307 *//*0008*/ MOV R1, c[0x0][0x44];/* 0x2800400110005de4 *//*0010*/ S2R R0, SR_TID.X;/* 0x2c00000084001c04 *//*0018*/ S2R R3, SR_CTAID.X;/* 0x2c0000009400dc04 *//*0020*/ IMAD R0, R3, c[0x0][0x28], R0;/* 0x20004000a0301ca3 *//*0028*/ MOV32I R3, 0x4;/* 0x180000001000dde2 *//*0030*/ IADD32I R2, R0, -0x6;/* 0x0bffffffe8009c02 *//*0038*/ I2F.F32.U32 R4, R0;/* 0x1800000001211c04 *//*0040*//* 0x22c042e04282c2c7 *//*0048*/ ISETP.GE.U32.AND P0, PT, R2, 0x4, PT;/* 0x1b0ec0001021dc03 *//*0050*/ @P0 ISCADD R2.CC, R0, c[0x0][0x148], 0x2;/* 0x4001400520008043 *//*0058*/ @P0 IMAD.U32.U32.HI.X R3, R0, R3, c[0x0][0x14c];/* 0x208680053000c043 *//*0060*/ @P0 ST.E [R2], R4;/* 0x9400000000210085 *//*0068*/ @P0 EXIT;/* 0x80000000000001e7 *//*0070*/ ISCADD R2.CC, R0, c[0x0][0x140], 0x2;/* 0x4001400500009c43 *//*0078*/ MOV32I R3, 0x4;/* 0x180000001000dde2 *//*0080*//* 0x2000000002e04287 *//*0088*/ IMAD.U32.U32.HI.X R3, R0, R3, c[0x0][0x144];/* 0x208680051000dc43 *//*0090*/ ST.E [R2], R4;/* 0x9400000000211c85 *//*0098*/ EXIT;/* 0x8000000000001de7 *//*00a0*/ BRA 0xa0;/* 0x4003ffffe0001de7 *//*00a8*/ NOP;/* 0x4000000000001de4 *//*00b0*/ NOP;/* 0x4000000000001de4 *//*00b8*/ NOP;/* 0x4000000000001de4 */


Результат декомпиляции
void predicates(float *param_1,float *param_2) {  uint uVar1;    uVar1 = *&blockIdx.x * (int)_DAT_constants_00000028 + *&threadIdx.x;  if (uVar1 - 6 < 4) {    param_1[uVar1] = (float)uVar1;    return;  }  param_2[uVar1] = (float)uVar1;  return;}


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

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

Теперь ищем паттерны для мнемоники, операндов и модификаторов.

Для этого сравниваем биты (в двоичном представлении) между подозрительными инструкциями (или строками, если их так удобнее называть). Можно также воспользоваться тем, что выкладывают другие пользователи в своих вопросах на stackoverflow по типу помогите понять двоичный/sass/машинный код, задействовать туториалы (в т.ч. на китайском языке) и прочие ресурсы. Так, основной номер операции хранится в битах 58-63, но есть и дополнительные биты 0-4 (они различают инструкции I2F, ISETP, MOV32I), где-то вместо них 0-2 (для пренебрежения 3-4 битами в пустых инструкциях, в спецификации они отмечены как UNK).

Для регистров и чисел-констант можно экспериментировать с дизассемблером дабы найти все биты, влияющие на вывод дампа наподобие выложенного под спойлером. Все поля, которые мне удалось найти, есть в спецификации на Github, файл CUDA.slaspec, раздел token.

Затем нужно придумать адреса для регистров, опять же они есть на Github. Это нужно, т.к. на микроуровне Sleigh рассматривает регистры как глобальные переменные в пространстве с типом register_space, но т.к. их пространство не отмечено как inferable (и наверняка оно не может быть), то они в декомпиляторе становятся либо локальными переменными (чаще всего с интерфиксом Var, но иногда вроде был и префикс local), либо параметрами (префикс param_). SP так и не пригодился, нужен в основном формально для обеспечения работоспособности декомпилятора. PC (что-то вроде IP из x86) нужен для эмуляции.

Затем идут предикатовые регистры, что-то вроде флагов, но уже более general purpose, чем для заранее продиктованной цели, вроде переполнения, (не)равенства нулю и т.п.
Затем блокировочный регистр для моделирования связки инструкций ISCADD .CC и IMAD.HI, т.к. первая из них в моей реализации выполняет подсчет за себя и за вторую, чтобы избежать переноса части суммы в старшие 4 байта, т.к. это испортит декомпиляцию. Но тогда надо заблокировать следующий регистр до завершения операции IMAD.HI. Что-то подобное, т.е. разночтение официальной документации и ожидаемого вывода декомпилятора, уже было в модуле SPU для той же Ghidra.

Потом идут специальные регистры, которые пока что реализованы через cpool. В будущем я планирую их заменить на символы, определённые по умолчанию для какого нибудь inferable пространства. Это те самые threadIdx, blockIdx.

Затем привязываем переменные к полям dest, par0, par1, par2, res. Затем идут подтаблицы, а после них то, ради чего всё и затевалось основные (корневые) таблицы с главными инструкциями.

Здесь нужно строго следовать формату мнемоника-операнды, однако даётся послабление для модификаторов, которые, тем не менее, должны быть прикреплены к мнемонике либо к секции с операндами. Никакие другие форматы недопустимы, даже тот же Hexagon DSP asm придётся адаптировать к этому синтаксису, что впрочем не очень сложно.

Финальным этапом будет написание реализации для инструкций на языке микропрограммирования Pcode. Единственное, что хотелось бы отметить из первого примера, это инструкции ISCADD .CC и IMAD.HI, где первая из них берёт указатель на регистры и разыменовывает их как указатели на 8 байтов вместо 4. Это сделано намеренно для того, чтобы лучше приспособиться к декомпилятору и его поведению, несмотря на то, что написано в документации Nvidia про перенос части суммы.

Для второго примера лучше поставить галочку в настройках декомпилятора напротив надписи Simplify predication. Смысл в том, что предикаты это одно и то же условие для разных инструкций, по сути ни что иное как всем известное SIMD, либо его очередной аналог. Т.е. если предикатовый бит установлен, то инструкции выполняются, притом подряд.

Нужно также завести привычку сразу писать реализацию для каждой инструкции, а не только прототип (мнемоника-операнды), ведь есть ещё декомпилятор, эмулятор и другие анализаторы.
Но вообще написать реализацию на Pcode задача даже более простая, чем писать грамматику для декодера байтов. Быстро получалось исправлять реализацию для некоторых сложных инструкций из x86 (и не только), благодаря очень удобному промежуточному языку, единому мидлэнду (оптимизатор), 2 бэкэндам (в основном C; как альтернативный вариант Java/C#, больше похоже на последний, т.к. время от времени появляется goto, но не labeled break).
В следующих статьях, возможно, будут также фронтенды для managed языков, таких как DXBC, SPIR-V, они будут использовать бэкэнд Java/C#. Но пока что в планах только машинные коды, т.к. байткоды требуют особого подхода.

Проект
Ghidra

Справки:

Pcode
Sleigh
Подробнее..

SQL для девочек ( и не только)

06.07.2020 12:10:38 | Автор: admin
Для тех кто работает с данными в Excel зачастую встает проблема управления подключениями внешних таблиц к реляционным источникам. Да, Excel предоставляет здесь полный инструментарий, но не обеспечивает уровень комфорта и завышает планку требований к знаниям пользователей.

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

1) Создавать excel таблицы с подключением к ODBC, основываясь на кастомном SQL запросе

2) Опираясь на (1) создавать pivot отчеты

3) Инструментарий по динамическому анализу данных

Keep-Only
Remove-Only
Undo
Redo

4) Автопостроитель SQL запросов с множественным объединением таблиц с одинаковым наименованием колонок

5) Навигатор по Excel книге

6) Обновление данных в DB из Excel

На данный момент поддерживаются следующие ODBC источники
1) MsSQl
2) Oracle
3) MySql
4) Postgres
5) Vertica



image

В ближайших планах добавить поддержку облака, первым в очереди стоит ClickHouse

Данный проект OpenSource, код и дистрибутивы доступны

https://sourceforge.net/projects/in2sql/

Замечания и предложения
https://t.me/in2sql

ER
Подробнее..
Категории: C , Postgresql , Sql , Excel , Oracle , Mysql , In2sql , Dwh

Категории

Последние комментарии

© 2006-2020, personeltest.ru