...

суббота, 5 января 2019 г.

[Из песочницы] Работа Xamarin c SDK, написанном на C

Не так давно у меня был интересный проект на Xamarin Forms для нескольких платформ:
  • Android
  • iOS
  • UWP
  • MacOS

Нам было необходимо создать библиотеку, которая смогла бы подключаться к нескольким нашим проектам: Xamarin.Forms, Android на Java, Cordova, а также позволять сторонним разработчикам использовать наше SDK в своих проектах с минимальными усилиями для интеграции.

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

Правда в итоге оказалось достаточно тяжело «подружить» библиотеку на C с разными платформами на Xamarin платформе. В данной небольшой статье будет расписано как нам удалось это сделать, и возможно, кому-то это пригодится и позволит сэкономить время на проекте.
Для нашего Xamarin проекта мы сделали еще nuget пакет, который является оберткой над нашей C библиотекой и позволяет в одном месте вносить все необходимые изменения для расширения SDK, а также некоторым образом расширять сам SDK.

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

Расширения нативных файлов


  • Android — *.so файл;
  • Universal Windows Platform (UWP) — *.dll файл;
  • iOS — *.a файл (файл статической библиотеки, которая по факту является fat файлом, в котором будут хранится все необходимые нам архитектуры);
  • MacOS — *.dylib файл (файл динамической библиотеки)

Возможные архитектуры на разных платформах


Android
  • arm
  • arm64
  • x86
  • x64

UWP
iOS
  • armv7
  • armv7s
  • i386
  • x86_64
  • arm64

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

Сборка и подготовка нативных файлов SDK


Universal Windows Platform (UWP)


Собираем проект на С для двух архитектур x86 и x64. После этого у нас будет два *.dll файла, которые нам и нужны.

Android


Чтобы создать нативные файлы для Android проекта. Нам нужно создать Xamarin С++ проект. Добавить наши С файлы и файлы с хедерами в Shared проект. После этого надо собрать проект со всеми необходимыми архитектурами (arm, arm64, x86, x64). Это даст нам *.so файлы для Android проекта.

iOS


Чтобы создать нативные файлы для iOS проекта мы могли бы использовать тот же Xamarin С++ проект, что мы использовали для Android, но тут есть нюанс. Нам необходимо соединиться с MacOS, чтобы собрать С++ проект. Но для этого нам необходимо установить vcremote на MacOS. Правда после последних обновлений это сделать сейчас просто невозможно. Может позже Microsoft обратит на это внимание и исправит его установку, но сейчас это к сожалению не так.

Из-за этого нам придется пойти другим путем. В XCode нам надо создать Cocos Touch Static Library проект для iOS. Как это сделать, мы можем прочитать здесь. В этот проект мы добавляем наши файлы из C SDK и собираем проект два раза, чтобы получить нужный нам набор архитектур:

  • for iphone simulator
  • for iphone

Затем мы можем проверить какие архитектуры включены в наши сборки статической библиотеки, используя команду терминала на MacOS — «lipo». К примеру можем сделать такой вызов:
lipo -info /path_to_your_a_file/lib.a

Результат должен быть таким:
Architectures in the fat file: /path_to_your_a_file/lib.a are : armv7 armv7s i386 x86_64 arm6

После того как мы приготовили файлы статической библиотеки, мы можем их соединить в один fat файл, со списком всех архитектур в одном файле, снова используя терминальную команду:
lipo -create lib_iphone.a lib_iphone_simulator.a -output lib.a

MacOS


На MacOS все будет крайне просто. Нам надо переконвертировать файл статической библиотеки в динамическую, снова используя терминальную команду:
clang -fpic -shared -Wl, -all_load lib.a -o lib.dylib

И все. Мы получим нужный нам *.dylib файл.

Nuget пакет


Так как мы делали nuget пакет и в нем добавляли специфическую логику для Xamarin проекта, то нам необходимо было сделать враппер для C SDK. На C# для подключения C методов нам необходимо использовать атрибут DllImport. Но тут снова есть нюанс. Нам необходимо использовать const для пути нативного С файла. При этом у каждого проекта путь к файлу будет своим и даже название самого файла будет другое. Из-за этого нам пришлось немного изощриться и написать для этого свои обертки.

Итак, наш основной класс, который описывает методы С файла.

public abstract class BaseLibraryClass {
    public abstract int Init (IntPtr value);
}

Затем для каждой платформы нам необходимо имплементировать абстрактный класс.

Android

internal class BaseLibraryClassDroid : BaseLibraryClass {
    private const string Path = "lib";

    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    private static extern int InitExtern (IntPtr value);

    public override int Init (IntPtr value) => InitExtern (value);
}


Universal Windows Platform (UWP)

internal class BaseLibraryClassx64 : BaseLibraryClass {
    private const string Path = "lib/x64/lib";

    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    private static extern int InitExtern (IntPtr value);

    public override int Init (IntPtr value) => InitExtern (value);
}
internal class BaseLibraryClassx86 : BaseLibraryClass {
    private const string Path = "lib/x86/lib";

    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    private static extern int InitExtern (IntPtr value);

    public override int Init (IntPtr value) => InitExtern (value);
}

iOS

internal class BaseLibraryClassIOS : BaseLibraryClass {
    private const string Path = "__Internal";

    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    private static extern int InitExtern (IntPtr value);

    public override int Init (IntPtr value) => InitExtern (value);
}

MacOS

public class BaseLibraryClassMac : BaseLibraryClass {
    private const string Path = "lib";

    [DllImport (Path, EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl)]
    private static extern int InitExtern (IntPtr value);

    public override int Init (IntPtr value) => InitExtern (value);
}

Теперь нам необходимо сделать enum файл со списком платформ / архитектур:
public enum PlatformArchitecture {
    Undefined,
    X86,
    X64,
    Droid,
    Ios,
    Mac
}

И фабрику для использования внутри нашего враппера:
public class SdkCoreFactory {
    public static BaseLibraryClass GetCoreSdkImp () {
        switch (Init.PlatformArchitecture) {
            case PlatformArchitecture.Undefined:
                throw new BaseLibraryClassInitializationException ();
            case PlatformArchitecture.X86:
                return new BaseLibraryClassx86 ();
            case PlatformArchitecture.X64:
                return new BaseLibraryClassx64 ();
            case PlatformArchitecture.Droid:
                return new BaseLibraryClassDroid ();
            case PlatformArchitecture.Ios:
                return new BaseLibraryClassIOS ();
            case PlatformArchitecture.Mac:
                return new BaseLibraryClassMac ();
            default:
                throw new BaseLibraryClassInitializationException ();
        }
    }
}

Также нам нужен Init метод для настройки всего что мы создали внутри наших проектов на Xamarin.
public static class Init {
    public static PlatformArchitecture PlatformArchitecture { get; set; }
}

Подключение сгенерированных библиотек к проектам


Universal Windows Platform (UWP)


Мы копируем сгенерированные файлы библиотек в папки:
  • lib/x86/lib.dll
  • lib/x64/lib.dll

И устанавливаем при старте приложения в Init методе нашу архитектуру:
Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.X64;

Android


Для Android проекта нам необходимо поправить *.csproj файл, сохранить проект и скопировать *.so файлы в папки. В Android проекте, мы указываем название сгенерированного файла, так как пути к файлам мы прописываем в *.csproj файле. Также нам необходимо помнить следующее при копировании файлов в папки:
  • armeabi — arm *.so файл
  • armeabi-v7a — arm *.so файл
  • arm64-v8a — arm64 *.so файл
  • x86 — x86 *.so файл
  • x64 — x64 *.so файл

Изменения для *.csproj файла:
<ItemGroup>
    <AndroidNativeLibrary Include="lib\armeabi\lib.so">
        <Abi>armeabi</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="lib\armeabi-v7a\lib.so">
        <Abi>armeabi-v7a</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="lib\arm64-v8a\lib.so">
        <Abi>arm64-v8a</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="lib\x86\lib.so">
        <Abi>x86</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="lib\x86_64\lib.so">
        <Abi>x86_64</Abi>
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </AndroidNativeLibrary>
</ItemGroup>

И устанавливаем архитектуру для nuget пакета:
Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Droid;

iOS


Нужно добавить сгенерированный *.a fat файл в корневую папку проекта и установить дополнительные инструкции при компилировании проекта (iOS properties => iOS build => Additional mtouch arguments). Устанавливаем следующие инструкции:
-gcc_flags "-L${ProjectDir} -llib -force_load ${ProjectDir}/lib.a"

Также не забываем в свойствах к *.a файлу указать Build Action как None.

И снова устанавливаем архитектуру для nuget пакета:

Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Ios;

MacOS


Добавляем наш *.dylib файл в Native References проекта и прописываем нужную архитектуру:
Wrapper.Init.PlatformArchitecture = Wrapper.Enums.PlatformArchitecture.Mac;

После данных манипуляций проекты для всех наших платформ подхватили сгенерированные нативные файлы и мы смогли использовать все функции из нашего СДК внутри проекта.

Let's block ads! (Why?)

[Перевод] Интернет вещей… которые плачут по хорошему UI/UX

иероглифы

Египетские иероглифы или обозначения кнопок на стиралке?

Я думаю, что, чисто теоретически, мы бы уже могли подключить большинство существующих ныне устройств к интернету, этим и порожден растущий интерес к Интернету Вещей (IoT).

Получим ли мы от этого большую пользу — уже другой вопрос.

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

Конечно, лоток все еще требует ручного опорожнения, а в приложении нужно нажать на «reset» чтобы сбросить показания и начать вести отсчет заново. В этот момент, я задал себе вопрос: «Зачем?»

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

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

Тем не менее, мне еще предстоит найти вескую причину использования таких примочек (не считая сомнительного развлекательного аспекта). Я не думаю, что когда-нибудь «попрошу Алексу» дистанционно включить/выключить свет, находясь при этом в трех метрах от выключателя.


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

Реальный интернет вещей или просто чуть лучший UI?

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

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


Смартфон — это ведь идеальное универсальное место для размещения пользовательского интерфейса управления домашними устройствами — он всегда находится у нас под рукой.

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

Именно такими были видеомагнитофоны, когда они впервые появились (в 1970-х годах), такими являются большинство систем охранной сигнализации и по сей день.

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


контроллер

В моем собственном доме, самое вопиющее преступление против UI совершено производителем небольшого устройства, контролирующего водонагреватель

Его невинный и простенький вид скрывает более запутанную историю: он расположен прямо рядом с водонагревателем (что кажется логичным), примерно в 25 см от пола (менее логично, учитывая рост взрослого человека), на стене в затемненном шкафу, на таком расстоянии от нагревателя, которое требует от вас зажать голову между нагревателем и устройством. Мне сейчас за 40, и чтобы разглядеть вещи, находящиеся поблизости, мне нужно надеть очки. Использование фонарика для чтения с маленького экрана устройства затрудняется отражением на этом ЖК-дисплее, поэтому, в конечном итоге я использую ручное зеркало и фонарик, чтобы увидеть дисплей в обратном направлении. Излишне говорить, что без руководства (я пытался, но так и не смог найти его в интернете) сделать вывод о том, за что отвечает каждая кнопка (их всего 4 — с плохой маркировкой и выбором нескольких режимов), попутно балансируя в неудобном положении довольно проблематично.


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

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


Интернетизировано?

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

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

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

Разве подключения этих устройств через домашний Wi-Fi не достаточно, чтобы авторизовать смартфон, подключенный через тот же Wi-Fi?

Может быть, стоит использовать более стандартизированные домашние хабы и локальные сети, вместо того, чтобы подключать их к интернету. В таком случае, можно ли это назвать «Интранетом Вещей»?

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


Проще интерфейс, сложнее подключение

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

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

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


Более удобное управление доашней техникой

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

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

Может вместо простого подключения к Интернету, технология IoT может помочь управлять техникой, если мы также добавим туда лучший UI?

Let's block ads! (Why?)

В Европе одобрили Директиву об авторском праве — почему стриминговые платформы выступают против

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

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


Фото Hernán Piñera / CC BY-SA

Что это за Директива?


Директива об авторском праве была одобрена Европарламентом в сентябре 2018 года. Наибольший интерес в ней представляет статья №13. Она возлагает ответственность за нарушения любых авторских прав на медиаплощадки, чем обязывает последние проверять весь загружаемый пользователями контент. На практике это означает, что сайты должны ввести систему автоматического распознавания нелицензионной музыки и видео.

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

Контент-платформы против статьи 13


Критики нового закона считают, что реализация контентных фильтров окажет негативное влияние на работу сервисов. Одна из причин — дороговизна таких решений. В том же YouTube на систему распознавания ContentID (реализованную задолго до появления закона) потратили более 60 млн долларов. Такие суммы могут оказаться неподъемными для небольших музыкальных сервисов.
Кроме того, непонятно, как именно автоматическая фильтрация будет реализована. Машина не сможет отличить музыку, изображения, видео или тексты, загруженные автором, от контента, загруженного кем-то другим. Это при условии, что реализация существующих систем оставляет желать лучшего: кейсов с неправильной работой фильтров очень много.

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

Подробнее позицию компании объяснила генеральный директор YouTube Сьюзен Войжитски (Susan Wojcicki). По её словам, с каверов на YouTube начинали карьеру такие музыканты, как Эд Ширан (Ed Sheeran) и Дуа Липа (Dua Lipa), а в самом популярном клипе на сайте — Despacito — не все использованные материалы принадлежат авторам. Статья 13 запрещает такое творчество, и поэтому закон приведет к тому, что талантливые исполнители не смогут найти свою аудиторию на YouTube.

Войжитски поддержал директор музыкального направления сайта Лиор Коэн (Lyor Cohen). По его словам, у половины музыкальных треков на YouTube известны не все правообладатели. Согласно Директиве, такой контент нужно блокировать. Это приведет к тому, что музыканты будут получать ещё меньше денег за свое творчество. Сейчас YouTube перечисляет исполнителям выплаты, сопоставимые по размеру с платежами Spotify, говорит Коэн.

Против нового законопроекта выступил и Twitch. Соучредитель компании Эммет Шир (Emmett Shear) написал открытое письмо, в котором призвал все сообщество подписать петицию против директивы Евросоюза. Текст письма опубликовали на Reddit. В нем сказано, что закон создаст массу неудобств для членов комьюнити со всего мира и не приведет к росту выплат для правообладателей. В Twitch будут вынуждены попросту ограничить доступ к трансляциям пользователей для жителей Европы.

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


Фото Nadine Heidrich / CC BY

Кто выступил «за»


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

По словам генерального директора Британской ассоциации производителей фонограмм Джеффа Тейлора (Geoff Taylor), правообладатели получают от YouTube в 16 раз меньше денег, чем от стриминговых сервисов. Он отметил, что текущие механизмы работы не эффективны, потому необходимо реализовывать версию, предложенную законодателями ЕС.

Что дальше


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

Хотя определённые шансы у медийных площадок остаются. Окончательное оформление редакции закона займет несколько месяцев (завершат в первой половине 2019 года). В это время Совет ЕС будет консультироваться с властями стран и изучать общественное мнение.



Дополнительное чтение — наш Мир Hi-Fi и тг-микроформат о звуке:

Окна с активным шумоподавлением заглушат звуки мегаполиса
​Несостоявшийся полёт на Луну: что рассказывает неизвестная ранее запись
Музыкальные дороги — что это и почему их нет в России

Let's block ads! (Why?)

Пишем свой язык программирования, часть 1: пишем языковую ВМ

Введение


Доброго времени суток всем хабрачитателям!

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

Я буду описывать создание языка, который описал ранее тут.

Он заинтересовал многих и вызвал бурную дискуссию в комментариях. Следовательно — тема интересна многим.

Думаю, что сразу стоит выложить информацию о проекте:

Сайт (будет заполнен документацией чуть позже).
Репозиторий

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

Кодить я умею на C/C++ и на Object Pascal. Проект я писал на FPC, т.к. на мой взгляд этот язык гораздо проще и лучше подходит для написание подобного. Вторым определяющим фактором стало то, что FPC поддерживает огромное количество целевых платформ и пересобрать проект под нужную платформу можно с минимумом переделок. Если вы по непонятным мне причинам не любите Object Pascal, то не спешите закрывать пост и бежать кидаться камнями в комментарии. Этот язык весьма красив и нагляден, а кода я буду приводить не так уж и много. Только то, что нужно.

Итак, начну пожалуй я своё повествование.

Ставим цели


Прежде всего, любому проекту нужны поставленные цели и ТЗ, которые придется в будущем реализовывать. Нужно заранее определиться, какого типа язык будет создаваться, чтобы написать первичную ВМ для него.

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

  • Динамическая типизация и приведение типов. Её поддержку я решил организовать на этапе разработки вм.
  • Поддержка многопоточности. Включил этот пункт в этот список заранее, чтобы должным образом спроектировать архитектуру ВМ и организовать поддержку многопоточности на уровне ядра ВМ, а не в дальнейшем с помощью костылей.
  • Экспорт внешних методов. Без этого язык будет бесполезен. Разве что его встраивать в какой-нибудь проект.
  • Компилируемость языка (в цельный абстрактный исполняемый файл). Частично компилируемый или интерпретируемый? От этого многое зависит.
  • Общая архитектура ВМ. Стековая или регистровая будет наша ВМ? Я попробовал реализовать и то и то. Выбрал для поддержки стековую ВМ.
  • Как вы видите работу с переменными, массивами, структурами? Лично я в тот момент хотел реализовать язык, в котором автоматически почти все завязывается на неявных указателях, ведь такой подход сильно экономил бы память и упрощал жизнь разработчику. Если мы допустим передаем в методы что-нибудь большое, то автоматом передастся лишь указатель на это большое.

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

Сразу скажу, что ВМ я назвал максимально красноречиво — SVM (Stack-based Virtual Machine).

Начнем, пожалуй, с реализации класса переменной


Изначально я просто использовал variant тип, потому что так проще и быстрее. Это был костыль, но он подпирал проект и позволил мне быстренько реализовать первую версию ВМ и языка. Позже я засел за код и написал реализацию своего «variant». По-сути нужно написать класс, который хранит указатель на значение в памяти, в моей реализации это null/cardinal/int64/double/string/array. Можно было бы использовать case типизацию, но я посчитал, что будет лучше реализовать так, как я реализовал.

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

П.с. для тех, кто может быть не в курсе, в чем разница между H- и H+ режимом FPC.

При сборке кода в режиме H- строки будут представлены в виде массива символов. При H+ — в виде указателя на кусок памяти. В первом случае строки будут изначально фиксированной длины и ограничены по дефолту 256 символами. Во втором случае — строки будут динамически расширяемыми и в них можно будет запихнуть гораздо больше символов. Будут работать немного медленнее, зато более функционально. При H+ можно также объявлять строки как массив символов, например таким вот образом:

var s:string[256];

Итак, для начала объявим Enum тип, который будем использовать как некий флажок, для определения типа данных по указателю:
type
  TSVMType = (svmtNull, svmtWord, svmtInt, svmtReal, svmtStr, svmtArr);


Далее опишем основную структуру нашего типа переменной и некоторые методы:
  TSVMMem = class
    m_val: pointer;
    m_type: TSVMType;
    constructor Create;
    destructor Destroy;
    procedure Clear;
  end;

...

constructor TSVMMem.Create;
begin
  m_val := nil;
  m_type := svmtNull;
end;

destructor TSVMMem.Destroy;
begin
  Clear;
end;

procedure TSVMMem.Clear; inline;
begin
  case m_type of
    svmtNull: { nop };
    svmtWord: Dispose(PCardinal(m_val));
    svmtInt:  Dispose(PInt64(m_val));
    svmtReal: Dispose(PDouble(m_val));
    svmtStr:  Dispose(PString(m_val));
    svmtArr:  begin
                SetLength(PMemArray(m_val)^, 0);
                Dispose(PMemArray(m_val));
              end;
    else
      Error(reVarInvalidOp);
  end;
end;


Класс ни от чего не наследуется, поэтому inherited вызовы в конструкторе и деструкторе можно не делать. Уделю внимание директиве inline. В заголовок файла лучше добавить {$inline on}, чтоб наверняка. Её активное использование в ВМ довольно ощутимо повысило производительность (мб где-то аж на 15-20%!). Она говорит компилятору, что тело метода лучше встроить на место его вызова. Выходной код будет немного больше в итоге, но работать будет быстрее. В данном случае, использование inline целесообразно.

Ок, запилили мы на этом этапе основу нашего класса. Теперь нужно описать ряд сеттеров и геттеров (setter & getter) у нашего класса.

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

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

procedure TSVMMem.SetV(const value; t:TSVMType); inline;
begin
  if (m_val <> nil) and (m_type = t) then
   begin
     case t of
       svmtWord: PCardinal(m_val)^ := Cardinal(value);
       svmtInt:  PInt64(m_val)^ := Int64(value);
       svmtReal: PDouble(m_val)^ := Double(value);
       svmtStr:  PString(m_val)^ := String(value);
     end;
   end
  else
   begin
     if m_val <> nil then
      FreeMem(m_val);

     m_type := t;
     case t of
       svmtWord: begin
                   New(PCardinal(m_val));
                   PCardinal(m_val)^ := Cardinal(value);
                 end;
       svmtInt:  begin
                   New(PInt64(m_val));
                   PInt64(m_val)^ := Int64(value);
                 end;
       svmtReal: begin
                   New(PDouble(m_val));
                   PDouble(m_val)^ := Double(value);
                 end;
       svmtStr:  begin
                   New(PString(m_val));
                   PString(m_val)^ := String(value);
                 end;
       else
         Error(reVarTypeCast);
     end;
   end;
end;

...

procedure TSVMMem.SetW(value:cardinal); inline;
begin
  if (m_val <> nil) and (m_type = svmtWord) then
   PCardinal(m_val)^ := value
  else
   begin
     if m_val <> nil then
      FreeMem(m_val);

     m_type := svmtWord;
     New(PCardinal(m_val));
     PCardinal(m_val)^ := value;
   end;
end;


Теперь можно и для пары геттеров написать код:
function TSVMMem.GetW:cardinal; inline;
begin
  Result := 0;
  case m_type of
    svmtWord: Result := PCardinal(m_val)^;
    svmtInt:  Result := PInt64(m_val)^;
    svmtReal: Result := Trunc(PDouble(m_val)^);
    svmtStr:  Result := StrToQWord(PString(m_val)^);
    else
      Error(reVarTypeCast);
  end;
end;


Ок, замечательно, теперь, после того, как вы просидели некоторое время пялясь в IDE и с энтузиазмом печатая код сеттеров и геттеров, мы стоим перед задачей реализации поддержки нашим типом математических и логических операций. В качестве примера я приведу реализацию операции сложения:
procedure TSVMMem.OpAdd(m:TSVMMem); inline;
begin
  case m_type of
    svmtWord: case m.m_type of
                svmtWord: SetW(GetW             + m.GetW);
                svmtInt:  SetI(GetW             + m.GetI);
                svmtReal: SetD(GetW             + m.GetD);
                svmtStr:  SetD(GetW             + StrToFloat(m.GetS));
                else
                  Error(reVarInvalidOp);
              end;

    svmtInt:  case m.m_type of
                svmtWord: SetI(GetI             + m.GetW);
                svmtInt:  SetI(GetI             + m.GetI);
                svmtReal: SetD(GetI             + m.GetD);
                svmtStr:  SetD(GetI             + StrToFloat(m.GetS));
                else
                  Error(reVarInvalidOp);
              end;

    svmtReal: case m.m_type of
                svmtWord: SetD(GetD             + m.GetW);
                svmtInt:  SetD(GetD             + m.GetI);
                svmtReal: SetD(GetD             + m.GetD);
                svmtStr:  SetD(GetD             + StrToFloat(m.GetS));
                else
                  Error(reVarInvalidOp);
              end;

    svmtStr:  case m.m_type of
                svmtWord: SetS(GetS             + IntToStr(m.GetW));
                svmtInt:  SetS(GetS             + IntToStr(m.GetI));
                svmtReal: SetS(GetS             + FloatToStr(m.GetD));
                svmtStr:  SetS(GetS             + m.GetS);
                else
                  Error(reVarInvalidOp);
              end;
    else
      Error(reVarInvalidOp);
  end;
end;


Все просто. Аналогичным образом можно описать и дальнейшие операции и вот наш класс готов.
Для массивов ещё конечно нужны пара методов, пример получения элемента по индексу:
function  TSVMMem.ArrGet(index: cardinal; grabber:PGrabber): pointer; inline;
begin
  Result := nil;
  case m_type of
    svmtArr: Result := PMemArray(m_val)^[index];
    svmtStr: begin
               Result := TSVMMem.CreateFW(Ord(PString(m_val)^[index]));
               grabber^.AddTask(Result);
             end;
    else
      Error(reInvalidOp);
  end;
end;


Супер. Теперь мы можем двигаться дальше.

Реализуем стек


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

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

Привожу реализацию стека целиком:

type
  TStack = object
  public
    items: array of pointer;
    size, i_pos: cardinal;
    parent_vm: pointer;
    procedure init(vm: pointer);
    procedure push(p: pointer);
    function peek: pointer;
    procedure pop;
    function popv: pointer;
    procedure swp;
    procedure drop;
  end;

  PStack = ^TStack;

  procedure TStack.init(vm: pointer);
  begin
    SetLength(items, StackBlockSize);
    i_pos := 0;
    size := StackBlockSize;
    parent_vm := vm;
  end;

  procedure TStack.push(p: pointer); inline;
  begin
    items[i_pos] := p;
    inc(i_pos);
    if i_pos >= size then
     begin
       size := size + StackBlockSize;
       SetLength(items, size)
     end;
  end;

  function TStack.peek: pointer; inline;
  begin
    Result := items[i_pos - 1];
  end;

  procedure TStack.pop; inline;
  begin
    dec(i_pos);
    if size - i_pos > StackBlockSize then
     begin
       size := size - StackBlockSize;
       SetLength(items, size);
     end;
  end;

  function TStack.popv: pointer; inline;
  begin
    dec(i_pos);
    Result := items[i_pos];
    if size - i_pos > StackBlockSize then
     begin
       size := size - StackBlockSize;
       SetLength(items, size);
     end;
  end;

  procedure TStack.swp; inline;
  var
    p: pointer;
  begin
    p := items[i_pos - 2];
    items[i_pos - 2] := items[i_pos - 1];
    items[i_pos - 1] := p;
  end;

  procedure TStack.drop; inline;
  begin
    SetLength(items, StackBlockSize);
    size := StackBlockSize;
    i_pos := 0;
  end;


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

Итак, как с тем, как устроен стек вы ознакомились. Таким же образом устроен callback стек, для простоты и удобства call & return операций и стек сборщика мусора. Единственное — другие размеры блоков.

Поговорим о мусоре


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

Первым делом хочу рассказать о том, как устроены сборщики мусора в других языках, например в Lua, Ruby, Java, Perl, PHP и т.д. Они работают по принципу подсчета указателей на объекты в памяти.

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

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

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

Итак, теперь разберемся с компиляцией в абстрактный исполняемый файл


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

Для этого нужно определить формат исполняемых файлов. У меня получилось следующее:

  1. Заголовок, например «SVMEXE_CNS».
  2. Секция, содержащая список библиотек, из которых будут импортироваться методы.
  3. Секция импорта нужных методов, библиотеки из которых импортируются методы указываются по их номеру в секции выше.
  4. Секция констант.
  5. Секция кода.

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

Выполнение кода


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

Набор опкодов — инструкций для виртуальной машины с небольшими комментариями я показываю заранее ниже:

type
  TComand = (
    {** for stack **}
    bcPH,     // [top] = [var]
    bcPK,     // [var] = [top]
    bcPP,     // pop
    bcSDP,    // stkdrop
    bcSWP,    // [top] <-> [top-1]

    {** jump's **}
    bcJP,     // jump [top]
    bcJZ,     // [top] == 0 ? jp [top-1]
    bcJN,     // [top] <> 0 ? jp [top-1]
    bcJC,     // jp [top] & push callback point as ip+1
    bcJR,     // jp to last callback point & rem last callback point

    {** for untyped's **}
    bcEQ,     // [top] == [top-1] ? [top] = 1 : [top] = 0
    bcBG,     // [top] >  [top-1] ? [top] = 1 : [top] = 0
    bcBE,     // [top] >= [top-1] ? [top] = 1 : [top] = 0

    bcNOT,    // [top] = ![top]
    bcAND,    // [top] = [top] and [top-1]
    bcOR,     // [top] = [top] or  [top-1]
    bcXOR,    // [top] = [top] xor [top-1]
    bcSHR,    // [top] = [top] shr [top-1]
    bcSHL,    // [top] = [top] shl [top-1]

    bcNEG,    // [top] = -[top]
    bcINC,    // [top]++
    bcDEC,    // [top]--
    bcADD,    // [top] = [top] + [top-1]
    bcSUB,    // [top] = [top] - [top-1]
    bcMUL,    // [top] = [top] * [top-1]
    bcDIV,    // [top] = [top] / [top-1]
    bcMOD,    // [top] = [top] % [top-1]
    bcIDIV,   // [top] = [top] \ [top-1]

    bcMV,     // [top]^ = [top-1]^
    bcMVBP,   // [top]^^ = [top-1]^
    bcGVBP,   // [top]^ = [top-1]^^
    bcMVP,    // [top]^ = [top-1]

    {** memory operation's **}
    bcMS,     // memory map size = [top]
    bcNW,     // [top] = @new
    bcMC,     // copy [top]
    bcMD,     // double [top]
    bcRM,     // rem @[top]
    bcNA,     // [top] = @new array[  [top]  ] of pointer
    bcTF,     // [top] = typeof( [top] )
    bcSF,     // [top] = sizeof( [top] )

    {** array's **}
    bcAL,     // length( [top] as array )
    bcSL,     // setlength( [top] as array, {stack} )

    bcPA,     // push ([top] as array)[top-1]
    bcSA,     // peek [top-2] -> ([top] as array)[top-1]

    {** memory grabber **}
    bcGPM,    // add pointer to TMem to grabber task-list
    bcGC,     // run grabber

    {** constant's **}
    bcPHC,    // push copy of const
    bcPHCP,   // push pointer to original const

    {** external call's **}
    bcPHEXMP, // push pointer to external method
    bcINV,    // call external method
    bcINVBP,  // call external method by pointer [top]

    {** for thread's **}
    bcPHN,    // push null
    bcCTHR,   // [top] = thread(method = [top], arg = [top+1]):id
    bcSTHR,   // suspendthread(id = [top])
    bcRTHR,   // resumethread(id = [top])
    bcTTHR,   // terminatethread(id = [top])

    {** for try..catch..finally block's **}
    bcTR,     // try @block_catch = [top], @block_end = [top+1]
    bcTRS,    // success exit from try/catch block
    bcTRR,    // raise exception, message = [top]

    {** for string's **}
    bcSTRD,     // strdel
    bcCHORD,
    bcORDCH,

    {** [!] directly memory operations **}
    bcALLC,  //alloc memory
    bcRALLC, //realloc memory
    bcDISP,  //dispose memory
    bcGTB,   //get byte
    bcSTB,   //set byte
    bcCBP,   //mem copy
    bcRWBP,  //read word
    bcWWBP,  //write word
    bcRIBP,  //read int
    bcWIBP,  //write int
    bcRFBP,  //read float
    bcWFBP,  //write float
    bcRSBP,  //read string
    bcWSBP,  //write string

    bcTHREXT,//stop code execution

    bcDBP    //debug method call
    );


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

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

Имеет указатель на массив с опкодами, IP (Instruction Pointer) — смещение выполняемой инструкции и указатели на прочие структуры ВМ.

Выполнение кода идет большим switch-case.

Просто приведу описание ВМ:

type
  TSVM = object
  public
    ip, end_ip: TInstructionPointer;
    mainclasspath: string;
    mem: PMemory;
    stack: TStack;
    cbstack: TCallBackStack;
    bytes: PByteArr;
    grabber: TGrabber;
    consts: PConstSection;
    extern_methods: PImportSection;
    try_blocks: TTRBlocks;
    procedure Run;
    procedure RunThread;
    procedure LoadByteCodeFromFile(fn: string);
    procedure LoadByteCodeFromArray(b: TByteArr);
  end;


Немного об обработке исключений


Для этого в ВМ есть стек обработчиков исключений и большой try/catch блок, в который завернуто выполнение кода. С стек можно положить структуру, которая имеет смещение точек входа на catch и finally/end блока обработки исключений. Также я предусмотрел опкод trs, который ставится перед catch и перебрасывает код на finally/end, если он выполнился успешно, попутно удаляя блок с информацией об обработчиках исключений с вершины соответствующего стека. Просто? Просто. Удобно? Удобно.

Поговорим о внешних методах и библиотеках


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

Первым делом в реализации ВМ объявим тип внешнего метода и протокол его вызова.

type
  TExternalFunction = procedure(PStack: pointer); cdecl;
  PExternalFunction = ^TExternalFunction;


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

Вызов в дальнейшем происходит таким вот образом в процессе выполнения кода:

TExternalFunction(self.extern_methods^.GetFunc(TSVMMem(self.stack.popv).GetW))(@self.stack);


Напишем простую библиотеку для нашей ВМ


И пусть она будет реализовывать для начала метод Sleep:
library bf;
{$mode objfpc}{$H+}

uses SysUtils, svm_api in '..\svm_api.pas';

procedure DSleep(Stack:PStack); cdecl;
begin
  sleep(TSVMMem(Stack^.popv).GetW);
end;

exports DSleep name 'SLEEP';

end. 


Итоги


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

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

Полный код ВМ доступен в репозитории, в ветке /runtime/svm.

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

Если вам что-то непонятно — то добро пожаловать в комментарии или на форум.

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

Let's block ads! (Why?)

[Перевод] Мысли о современном C++ и игровой разработке

Новый год для игровых разработчиков начался с волны критики, обрушившейся в адрес комитета по стандартизации C++ после публикации Араса Пранкевичуса «Жалобы по поводу современного C++». Возник серьезный вопрос: действительно ли комитет стандартов потерял связь с реальностью, или все наоборот, и это игровые разработчики откололись от остального С++ сообщества?

Вашему вниманию предлагается перевод популярного поста Бена Дина, — ветерана игровой индустрии, проработавшего продолжительный срок в компаниях Blizzard, Electronic Arts и Bullfrog в качестве разработчика на C++ и тимлида, — в котором он отвечает на критику с позиции собственного опыта.

TL;DR: Комитет по стандартизации C++ не имеет скрытой цели игнорировать нужды игровых разработчиков, а «современный» C++ не собирается становиться «неотлаживаемым» языком.
На протяжении всей прошлой недели в Twitter шла активная дискуссия, в ходе которой многие программисты – особенно те из них, кто работает в сфере игровой разработки – высказались о том, что нынешний вектор развития «современного C++» не отвечает их потребностям. В частности, с позиции обычного игрового разработчика, все выглядит так, будто производительность отладки в языке игнорируется, а оптимизация кода становится ожидаемой и необходимой.

В силу того, что на 2019 год я успел проработать в игровой индустрии более 23 лет, у меня имеется собственное мнение, основанное на наблюдениях по данной теме применительно к игровой разработке, которым мне и хотелось бы поделиться. Важна ли для игровых разработчиков «отлаживаемость» и почему? В чем заключаются вопросы, связанные с ней?

Для начала — небольшой экскурс в историю.
Многие игровые разработчики, пишущие на C++, работают в Microsoft Visual C++. Исторически сложилось, что вокруг платформ Microsoft сформировался огромный рынок для игр, и это отразилось на типичном опыте рядового игрового программиста. В 90-ые и 2000-ые большинство игр писалось с учетом этих обстоятельств. Даже с появлением консолей других производителей и ростом популярности мобильных игр, достоянием многих AAA-студий и многочисленных игровых программистов на сегодняшний день являются инструменты, произведенные Microsoft.

Visual Studio — это, возможно, самый лучший отладчик для C++ на свете. Причем сильнее всего Visual Studio действительно выделяется именно по части отладки программ — больше, чем своими front-end, back-end, реализацией STL или чем-либо еще. В последние пять лет Microsoft добилась серьезных успехов в развитии инструментов для разработки на C++, но даже и до этих заслуг отладчик в Visual Studio всегда был очень крутым. Так что когда вы занимаетесь разработкой на ПК с Windows, у вас под рукой всегда есть дебаггер мирового класса.

Учитывая вышесказанное, давайте рассмотрим процесс получения кода, в котором не будет багов; имеющиеся у нас возможности с точки зрения программиста, который не занимается играми; а также ограничения, с которыми сталкиваются игровые разработчики. Если перефразировать основной довод в пользу «вектора развития современного С++», то он сведется к типам, инструментам и тестам. Следуя этой мысли, отладчик должен выступать последней линией обороны. Прежде, чем мы ее достигнем, у нас имеются следующие возможности.

Возможность №1: Типы


Мы можем использовать столько сильной типизации, сколько потребуется, для того, чтобы исключить целые классы багов во время компиляции. Сильная типизация, вне сомнения, является возможностью, которую предоставила нам недавняя эволюция C++; например, начиная с C++11, мы успели получить:
  • значительное расширение type traits;
  • такие нововведения, как nullptr и scoped enum для борьбы с наследием C — слабой типизацией;
  • GSL и вспомогательные инструменты;
  • концепты в C++20.

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

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

Откровенно говоря, вы легко можете проигнорировать время компиляции — но только при условии, что вы — программист в очень большой компании, которая не занимается играми и обладает устоявшейся внутренней инфраструктурой и бесконечными вычислительными мощностями для того, чтобы скомпилировать любой код, который вы только можете написать. Подобные огромные компании обеспокоены стоимостью компиляции — поэтому используют модули — но, как правило, у отдельных разработчиков боли это не вызывает. В то же время, для большинства игровых программистов это совсем не так. У инди-разработчиков нет ферм для создания сборок; разработчики AAA-игр часто используют что-то вроде Incredibuild, но, учитывая тот факт, что они запросто могут работать с кодовой базой, которой исполнилось 10 и более лет, процесс сборки все еще может занимать 15-20 минут.

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

  • Аппаратные средства — это реальные разовые расходы, которые будут возложены на бюджет текущего квартала, в отличие от не столь осязаемых расходов во времени/найме/и тому подобном, которые будут распределены на больший период времени. Люди плохо справляются с решением в пользу такого компромисса, а компании специально построены таким образом, чтобы оптимизировать краткосрочную прибыль.
  • Инфраструктура требует поддержки, и почти никто не идет в игровую индустрию для того, чтобы стать релиз-инженером. В сравнении с другими областями, где используется C++, зарплата игровых разработчиков не настолько велика — а не-игровым инженерам платят здесь еще меньше.

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

Возможность №2: Инструменты


Мы должны использовать максимум доступных нам инструментов — предупреждения (warnings), статический анализ, санитайзеры, инструменты динамического анализа, профилировщики и прочие.

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

  • Данные инструменты имеют тенденцию лучше работать на платформах, не связанных с Microsoft — и, как упоминалось ранее, это не является типичным сценарием в игровой разработке.
  • Эти инструменты в большинстве своем нацелены на работу со «стандартным» C++. В них «из коробки» поддерживается std::vector, но не мой самописный класс CStaticVector из гипотетического движка. Безусловно, винить в этом инструменты бесполезно, но это по-прежнему один из барьеров в их использовании, который приходится преодолевать разработчикам.
  • Создание и поддержка CI-цепочки, которая будет запускать все эти инструменты, требует присутствия релиз-инженеров — и, как упоминалось ранее, найм людей на инженерные вакансии, не связанные непосредственно с играми, является системной проблемой для игровой индустрии.

Итак, раз эти инструменты так хорошо работают со стандартным C++, почему же игровые разработчики тогда не используют STL?

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

  • До начала 90-ых, мы не доверяли компиляторам C, поэтому мы писали игры на ассемблере.
  • С начала и до середины 90-ых, мы начали доверять компиляторам C, но мы все еще не доверяли компиляторам C++. Наш код представлял собой C, в котором применялись комментарии в стиле C++, и нам больше не требовалось все время писать typedef-ы для наших структур.
  • Около 2000 года в мире игровой разработки произошла революция C++. Это была эпоха паттернов проектирования и больших иерархий классов. В то время поддержка STL на консолях оставляла желать лучшего, а миром тогда правили именно консоли. На PS2 мы навеки застряли с GCC 2.95.
  • Примерно в 2010 году предпринимаются еще две революции. Боль от использования больших иерархий классов стимулировала разработку компонентного подхода к коду. Это изменение продолжает свою эволюцию и сегодня в виде архитектур Entity-Component-System. Рука об руку с этим шла вторая революция — попытка воспользоваться преимуществом многопроцессорных архитектур.

В ходе этих сдвигов парадигм постоянно изменялись и сами платформы игровой разработки, причем изменялись серьезно. Сегментированая память уступила место плоскому адресному пространству. Платформы стали многопроцессорными, симметричными и не очень. Игровым разработчикам, привыкшим работать с архитектурами Intel, пришлось привыкать к MIPS (Playstation), затем к специальному «железу» с гетерогенными CPU (PS2), после этого к PowerPC (XBox 360), затем к еще большей гетерогенности (PS3)… С каждой новой платформой приходили новые рабочие характеристики для процессоров, памяти и дисков. Если вы хотели добиться оптимальной производительности, то вы были вынуждены переписывать ваш старый код, причем много и часто. Я даже не стану упоминать то, как сильно на игры повлияло появление и рост популярности Интернета, а также ограничения, которые накладывали на разработчиков держатели платформ.

Исторически сложилось, что реализации STL на игровых платформах были неудовлетворительными. Не является секретом и то, что STL-контейнеры слабо подходят для игр. Если прижать игрового разработчика к стенке, то возможно он признается в том, что std::string — вполне себе ОК, и std::vector — разумный вариант по умолчанию. Но у всех контейнеров, содержащихся в STL, имеется проблема контроля аллокации и инициализации. Во многих играх приходится беспокоиться по поводу ограничения памяти для различных задач — и для тех объектов, память для которых скорее всего придется выделять динамически во время геймплея, часто используются slab или arena аллокаторы. Амортизированное константное время — недостаточно хороший результат, поскольку аллокация потенциально является одной из самых «дорогих» вещей, что могут произойти во время выполнения программы, и мне не хочется пропускать кадр только из-за того, что она произошла тогда, когда я этого не ожидал. Я, как игровой разработчик, должен управлять своими требованиями к памяти заранее.

Похожая история получается и для других зависимостей в общем. Игровые разработчики хотят знать, на что уходит каждый цикл процессора, где и когда и за что отвечает каждый байт памяти, а также где и когда контролируется каждый поток выполнения. До последнего времени, компиляторы Microsoft меняли ABI с каждым обновлением — поэтому, если у вас было много зависимостей, то перестраивание всех их могло быть болезненным процессом. Игровые разработчики обычно предпочитают небольшие зависимости, которые легко интегрируются, делают всего одну вещь и делают ее хорошо — желательно с API в стиле C — и при этом используются во многих компаниях, находятся в открытом доступе (public domain) или имеют бесплатную лицензию, которая не требует указания автора. SQLite и zlib — хорошие примеры того, что предпочитают использовать игровые разработчики.

Помимо этого, индустрия игр на С++ имеет богатую историю больных синдромом «Not invented here». Этого следует ожидать от индустрии, которая начиналась с одиночек-энтузиастов, которые мастерили что-то свое на совершенно новом оборудовании и не имели никаких других вариантов. Игровая индустрия, помимо прочего, единственная, где программисты указываются в титрах без определенного порядка. Писать разнообразные штуки весело, и это помогает вашей карьере! Гораздо лучше строить что-то свое, чем покупать готовое! А поскольку мы так беспокоимся о производительности, мы можем адаптировать наше решение таким образом, чтобы оно подходило именно для нашего проекта — вместо того, чтобы взять обобщенное решение, бездумно тратящее имеющиеся ресурсы. Неприязнь к Boost — основной пример того, как подобное мышление проявляется в игровой разработке. Я работал на проектах, которые прошли следующий путь:

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

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

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

Вполне разумно подыскать улучшенную альтернативу std::map, или применить small buffer optimization в std::vector. Гораздо менее приемлемо быть обреченным на поддержку своих собственных реализаций algorithms или type traits, что не принесет практически никакой пользы. Как по мне, прискорбно, что STL для большинства разработчиков — это одни лишь контейнеры. Поскольку при изучении STL на старте обучают именно им, то говоря об STL большинство подразумевает std::vector — хотя на самом деле им следовало бы думать про std::find_if.

Возможность №3: Тесты


Утверждается, что должно осуществляться экстенсивное тестирование, TDD и/или BDD должно покрывать весь код, который можно покрыть, а с багами необходимо бороться написанием новых тестов.

Поэтому давайте обсудим тему тестирования.

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

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


Будучи молодым программистом в игровой индустрии, я быстро избавился от мысли о том, что я должен стремиться моделировать что-либо реалистично. Игры — это пускание пыли в глаза (smoke and mirrors) и поиск коротких путуй. Никого не волнует, насколько реалистична ваша симуляция; главное, чтобы она была увлекательной. Когда у вас нет другой спецификации, кроме как «игра должна ощущаться правильно», отсутствует сам предмет тестирования. Благодаря багам геймплей может даже становиться лучше. Достаточно часто баг попадает в релиз, и даже завоевывает любовь пользователей (вспомните того же Ганди из Civilization). Игры отличаются от других сфер, в которых используется C++; здесь нехватка корректности не приводит к тому, что кто-то в итоге лишается своих сбережений.

2. Потому что это тяжело


Разумеется, вам хотелось бы производить автоматизированные тесты везде, где вы сможете. Это может быть осуществлено для некоторых подсистем, для которых есть четко сформулированные конечные результаты. Юнит-тестирование в игровой индустрии, конечно, присутствует, но как правило ограничивается низкоуровневым кодом — упомянутыми ранее аналогами STL, процедурами преобразования строк, методами физического движка и т.д. Те случаи, когда у исполняемого участка кода есть предсказуемые результаты, обычно тестируются юнит-тестами, хотя TDD здесь и не применяется — поскольку игровые программисты предпочитают упрощать себе жизнь, а не наоборот. Но как вы протестируете код геймплея (смотрите пункт первый)? Как только вы выходите за рамки юнит-тестирования, то сразу встречаетесь с еще одной причиной, почему тестирование игр является настолько сложным.

3. Потому что в него вовлечен контент


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

4. Потому что мы его не практикуем


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

5. Поскольку [компания] не видит необходимости в автоматизированном тестировании


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

6. Потому что в целом тестирование относят в играх к второсортной деятельности


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

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

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

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

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

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

Они вынуждены подчиняться. Тестировщикам часто говорят не беспокоить других инженеров. Когда им нужно сообщить о баге напрямую, их просят обращаться к инженерам уважительно, вроде «Миссис Х.» или «Мистер Y.». Иногда мне звонило раздраженное начальство QA-отделов — в тех случаях, когда я для совместного расследования связывался напрямую с теми, кто обнаруживал баг.

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

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

Производительность отладки


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

Но при этом остаются проблемы с самой отладкой, и проблемы с тем, как игровые разработчики справляются с нынешним вектором развития C++.

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

Другими словами, мы хотим иметь производительную отладку, потому что для того, чтобы ловить баги, нам часто нужно иметь возможность выполнять запуск приложений с достаточно большими и репрезентативными наборами данных. Но на самом деле, когда мы достигаем этой точки, то обычно дебаггер становится слишком грубым инструментом для использования — вне зависимости от того, производителен он или нет. Конечно, установка точек останова на данных (data breakpoints) может быть полезна для поимки проблем среднего размера, но что делать, если мы столкнемся с реальными багами — теми, которые остаются после того, как мы казалось бы уже все пофиксили? С теми самыми, которые возникают под нагрузкой в сети, или в случае нехватки памяти, или работающей на пределе возможностей многопоточности, или случаются лишь для небольшого, еще не идентифицированного подмножества на фоне миллиона других игроков, или возникают только на дисковых версиях игры, или только в сборке на немецком языке, или спустя три часа, проведенных за тестированием стабильности (soak testing)?

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

Зачастую, пока мы доберемся до настоящей причины «крэша», мы успеем исправить несколько других вещей. Другими словами, мы решаем проблемы, и в конце концов, использование дебаггера — это лишь небольшая часть данного процесса. Так что да, скорость отладки — приятное дополнение, но ее нехватка не мешает нам продолжать оставаться инженерами. Нам по-прежнему требуются другие наши навыки, вроде способностей анализировать дампы ядра и читать оптимизированный ассемблер.

При использовании «современного С++» я пользуюсь отладчиком тем же самым образом, что и обычно. Я прохожу им по свеженаписанному коду; ставлю брейкпоинты на тех данных, которые меня интересуют; использую дебаггер для того, чтобы исследовать незнакомый код. С приходом «современного C++» ничего из этого не меняется, — и да, даже несмотря на то, что STL использует _Уродливые _Идентификаторы, это не делает STL магией. Иногда бывает полезным посмотреть, что STL делает «под капотом», или же переступить через нее; или, как теперь можно сделать, использовать отладчик для того, чтобы скрыть код библиотеки от меня.

Когда я сталкиваюсь с проблемами производительности отладки, то дело обычно не в том, что «современный C++» замедляет меня — дело в том, что к этому моменту я уже пытаюсь сделать слишком много всего. Использование отладчика не масштабируется — в отличие от типов, инструментов и тестов.

Я сам был обеспокоен проблемой того, что код на C++ требует все больше и больше оптимизации, и я интересовался мнением разработчиков компиляторов по этому поводу. Факт состоит в том, что здесь нет однозначного ответа. Мы уже находимся в континууме, и у нас есть возможность продвигаться дальше в этом направлении без причинения вреда возможности отладки кода. Сегодня наши компиляторы выполняют copy elision (пропуск копии) для временных объектов, даже если мы не просим их производить данную оптимизацию. На нашу возможности отладки приложений это никак не влияет. Сомневаюсь, что мы станем жаловаться на то, что отладочные билды стали включать NRVO или еще полдюжины оптимизаций, которые могут быть произведены таким образом, что во время отладки мы их и не заметим. Подозреваю, что C++ движется как раз в этом направлении.

Эпилог: Путь современного С++


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

1. Ничего не делать


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

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

Однако, если вы работаете в большой компании, то рано или поздно вам придется столкнуться с изменениями в языке, поскольку вам придется увеличивать команду и нанимать новых людей. В свою очередь, когда вы будете нанимать C++-разработчиков, это будет означать найм разработчиков на «современном» C++. Произойдет смена поколений — как это уже приключилось с ассемблером, C и C++98. Вы сможете управлять процессом, если установите ограничения на то, что разрешено в вашей кодовой базе, а что – нет, но и это решение не спасет вас в долгосрочной перспективе. И что вам делать в таком случае?

2. Принять участие


Вместо того, чтобы раз в год ездить лишь на одну GDC, начните посещать CppCon, где вы получите гораздо большую пользу от денег, потраченных вашей компанией на билет. Участвуйте в обсуждениях стандартов; вступайте в группы и подписывайтесь на рассылки; читайте черновики стандартов и предоставляйте авторам обратную связь. Если вы сможете еще и посетить встречи комитета, то это будет просто отлично, но даже если и нет — вы все еще можете сделать многое для того, чтобы донести до других вашу точку зрения.

Участие в комитете по C++ открыто для всех. Вся необходимая информация для тех, кто хочет принять участие в работе SG14, или SG7, или SG15 — или любой другой рабочей группе, касающейся сферы ваших интересов — может быть найдена на isocpp.org. У комитета нет никаких тайных планов — в самом деле, вы и правда считаете, что свыше 200 программистов могут согласовать между собой единую повестку? Здесь даже у «начальства» комитета зачастую не удается «пропихнуть» свои идеи.

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

Let's block ads! (Why?)

[Перевод] Unreal Engine 4 — шейдер горения

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

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

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

Текстуры


Я использовал следующие текстуры в шейдере, первая, это простой noise, генерируемый в Substance Designer, вторая, flowmap, которую я нарисовал с помощью FlowMap Painter.

Настройка FlowMap


Flowmapping — это метод, с помощью которого мы используем 2D векторы, хранящиеся в виде текстуры, чтобы искажать UV текстуры в течении определенного времени. Эта анимация потом зацикливается, чтобы создать иллюзию потока. Обычно такое используют в потоках воды, чтобы создать впечатление, что вода течет вокруг камней и т.п. Но, конечно, его можно использовать и для множества других вещей. В моем шейдере я использую его для создания эффекта горения материала. Если вы хотите больше понять о flowmap, рекомендую почитать статью на Polycount и посмотреть урок от Simonschreibt, в котором он отлично объясняет работу с flowmap.

Мои вариант настроек ниже

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

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

SphereMask


Этот шаг может быть реализован несколькими способами, в конце концов, все, что ему нужно, это значение масштаба градиента, чтобы управлять нашим эффектом. В этом случае я использую SphereMask, управляемую положением blueprint’а, чтобы перемещать нашу маску. Двигая blueprint вокруг, я могу влиять на то, какие области сжигаются, а тaк же анимировать радиус сферы, чтобы создавать эффект сгорания. Если вы хотите узнать, как настроить набор параметров материала для передачи положения BP в шейдер, рекомендую почитать о Wormhole шейдере, где я подробно это описываю.

Создать SphereMask легко, так как UE4 уже имеет встроенную функцию

В этом случае радиус указан в мировых координатах (World Space), чтобы это увидеть, попробуйте изменить значение радиуса. По умолчанию расположение SphereMask равно {0, 0, 0}, поэтому вы заметите, что сфера появляется в центре меша.

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

Обратите внимание, что вы можете настроить параметр Hardness у SphereMask, чтобы контролировать величину разброса при переходе

Hardness: 0.2 Hardness: 0.5

Теперь мы можем взять результат и умножить его на Opacity_Mask и вернуть в наш канал Opacity материала, чтобы получить итоговую маску.

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

Edge Glow и Charring


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

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

Если вы поставите превью на каждую из этих нод, вы увидите следующее

EmberGlow Charring

Теперь берем результат из EmberGlow и умножаем на цвет пламени, далее добавляем его к цвету Emissive и возвращаем на вход нашего материала.

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

Затем мы берем результат из секции Charring и умножаем его на Basecolor из входных данных функции, чтобы получить значение Final Basecolor.

Теперь вы можете видеть как чернеют края, перед тем как начать гореть

На этом мы закончили.

Теперь самая крутая часть — мы можем перемещать наш blueprint вокруг, чтобы изменять область горения. К примеру в игре мы можем использовать расположение факела игрока, чтобы управлять этим эффектом. В результате это будет выглядеть так, как будто он что-то сжигает. И мы можем управлять параметром Hardness у SphereMask и радиусом, чтобы получить нужный нам вид. Вот так это будет выглядеть с разными настройками.

Mask Radius: 512
Spheremask Hardness:0
Mask Radius: 92
Spheremask Hardness:0.5

Кроме того, вы можете анимировать радиус, чтобы это выглядело так, как будто он сгорает со временем.

Пример проекта с данным шейдером можете взять из репозитория github

Let's block ads! (Why?)

Google удалось вывести из-под налогообложения $22,7 млрд через Ирландию и Бермуды

Изображение: Unsplash

Журналисты Reuters выяснили, что использование схемы налоговой оптимизации под названием «двойная ирландская с голландским сэндвичем» позволило Google в 2017 году вывести из-под налогообложения почти $23 млрд.

Как это работает


Для реализации схемы оптимизации используются дочерние компании в Ирландии, Нидерландах и на Бермудах. Деньги проходят от ирландской компании через голландскую («голландский сендвич») на Бермуды, а затем средства вновь возвращаются в Ирландию на счет уже другой «дочки» («двойная ирландская»).

Журналисты Reuters получили доступ к документам нидерландской Торговой палаты, согласно которым, таким образом Google удалось вывести из под налогообложения в США и Европе $22,7 млрд.

Подобным образом Google удается уклоняться от уплаты как американского налога на прибыль корпораций – на Бермудах его нет – так и от европейских налогов на репатриацию доходов. Что интересно, в 2017 году нидерландская «дочка» Google заплатила налогов на €3,4 млн.

Перспективы схемы


По данным СМИ, в 2016 году Google с помощью налоговой схемы удалось увести от налогообложения около €16 млрд.
«Как и любая другая транснациональная компания, Google платит подавляющее большинство своего корпоративного подоходного налога на родине, а глобальная эффективная налоговая ставка за последние 10 лет составила 26%. Мы платим все налоги и соблюдаем налоговое законодательство в каждой стране, в которой работаем по всему миру» – прокомментировали в Google использование схем налоговой оптимизации.

Несмотря на эффективность описанной схемы, ее будущее может быть под угрозой. Властям ЕС и США не очень нравится, что крупные компании недоплачивают налоги из-за существующих лазеек в законодательстве. В том числе поэтому, в 2014 году Ирландия под давлением Евросоюза и США решила прекратить предоставлять Google налоговые преимущества начиная с 2020 года.

Другие материалы по теме финансов и фондового рынка от ITI Capital:


Let's block ads! (Why?)

[Перевод] DEFCON 21. Одних паролей недостаточно, или почему «ломается» шифрование диска и как это можно исправить. Часть 1

Спасибо всем, что пришли, сегодня мы поговорим о полном шифровании жёсткого диска (FDE), которое не так безопасно, как вы думаете. Поднимите руки, кто шифрует таким образом HDD своего компьютера. Поразительно! Ну что же, добро пожаловать на DefCon!

Похоже на то, что 90% из вас используют для шифрования диска программы с открытым исходным кодом, чтобы иметь возможность провести их аудит. Теперь пусть поднимут руки те, кто полностью выключает компьютер, если оставляет его без присмотра. Я думаю, примерно 20% присутствующих. Скажите, а кто вообще оставляет свой компьютер без присмотра на несколько часов, всё равно, включён он или выключен? Считайте, что я задаю эти вопросы, просто убедиться, что вы не зомби и не спите. Я думаю, что практически всем приходилось оставлять свой компьютер хотя бы на несколько минут.

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

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

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

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

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

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

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

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

Реальное использование шифрования не совпадает с моделью безопасности FDE. Если рассмотреть ПО, предназначенное для полного шифрования диска, видно, что его создатели уделяли очень много внимания теоретическим аспектам шифрования. Я приведу выдержку из технической документации с веб-сайта программы TrueCrypt: «Наша программа не защитит никакие данные на компьютере, если атакующий имеет физический доступ к компьютеру до запуска или в процессе работы TrueCrypt».

В принципе, вся их модель безопасности выглядит так: «если наша программа правильно шифрует диск и правильно его расшифровывает диск – мы сделали свою работу». Извиняюсь за текст, который изображён на следующем слайде, если вам трудно его прочесть, я сделаю это сам. Это выдержки из переписки между разработчиками TrueCrypt и исследователем проблем безопасности Джоанной Рутковской по поводу атак «уборщика».

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

Джоанна Рутковская спросила их: «Как можно определить, «поработал» ли злоумышленник над вашим компьютером или нет, ведь вы не носите ноутбук всё время с собой?», на что разработчики ответили: «Нас не волнует, каким образом пользователь обеспечивает сохранность и неприкосновенность своего компьютера. Например, пользователь мог бы использовать замок или помещать ноутбук на время своего отсутствия в запираемый шкаф или сейф». Джоанна ответила им очень корректно: «Если я собираюсь пользоваться замком или сейфом, зачем мне вообще ваше шифрование»?

Таким образом, игнорирование возможности подобной атаки – это обман, мы не можем так поступать! Мы живем в реальном мире, где существуют эти системы, с которыми мы взаимодействуем и которые используем. Не существует никакого способа сравнить 10 минут атаки, выполняемой только с помощью программного обеспечения, например, с «флешки», с чем-то, что вы можете выполнять, манипулируя системой исключительно с помощью аппаратных средств.
Таким образом, независимо от того, что они говорят, физическая безопасность и устойчивость к физическим атакам зависит от FDE. Не имеет значения, от чего вы отказываетесь в своей модели безопасности, и, по крайней мере, если они не хотят брать на себя ответственность, то им следует предельно ясно и честно объяснить пользователю, как легко может быть взломана защита, которую они предлагают.

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

Как мы знаем, загрузчик грузится с SSD/HDD с помощью BIOS и копируется в основную память по пути передачи данных Storage Controller – PCI Bus – Platform Controller Hub. Затем загрузчик запрашивает у пользователя данные аутентификации, такие как пароль или ключ смарт-карты. Далее пароль проходит путь от клавиатуры к процессору, после чего загрузчик принимает управление на себя, при этом оба компонента – ОС и ключ — остаются в памяти для того, чтобы обеспечить прозрачность процесса шифрования и дешифровки диска. Это идеализированный взгляд на процесс, предполагающий, что никто не попытается каким-то образом в него вмешаться. Я думаю, что вы прекрасно знаете несколько способов, как это можно взломать, поэтому давайте перечислим вещи, которые могут пойти не так, если кто-то попытается на вас напасть. Я подразделяю атаки на 3 уровня.

Первый – неинвазионный, не требующий захвата вашего компьютера, так как осуществляется с помощью флеш-накопителя с вредоносным ПО. Вам не нужно «разбирать» систему, если вы можете без проблем подсоединить к ней любой аппаратный компонент типа PCI card, ExpressСard или Thunderbolt – новейший адаптер Apple, предоставляющий открытый доступ к шине PCI.

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

Одним из типов атак первого уровня является скомпрометированный загрузчик, также известный под названием атака «evil maid», где вам нужно выполнить некоторый незашифрованный код в рамках процесса загрузки системы, что-то, что вы можете загрузить сами, используя персональную информацию пользователя, чтобы затем получить доступ к остальной части данных, зашифрованных на жестком диске. Есть несколько различных способов сделать это. Вы можете физически изменить загрузчик в системе хранения. Вы можете скомпрометировать BIOS или загрузить вредоносный BIOS, который возьмёт под контроль адаптер клавиатуры или процедуры чтения диска и модифицирует их так, чтобы они были устойчивы к удалению жесткого диска. Но в любом случае, вы можете модифицировать систему так, что когда пользователь вводит свой пароль, он будет записываться на диск в незашифрованном виде, или проделать что-то подобное, это не составит особого труда для хакера.

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

Это также может произойти при атаке системы эксплойтом, благодаря которому атакующий получает root-права и может прочитать ключ из основной памяти, это весьма распространённый способ атаки. Этот ключ может быть сохранен на жестком диске в виде открытого текста для последующего применения злоумышленником или отправлен по сети системе Command&Control.
Другая возможность – это клавиатурный перехват с помощью кейлоггера, будь то программное обеспечение, аппаратное обеспечение или что-то экзотическое, типа камеры «игольное ушко» или например, микрофон, который записывает звуки, сопровождающие нажатия клавишей пользователем, и пытается выяснить, что это за клавиши. Предотвратить такую атаку трудно, потому что потенциально она включает в себя компоненты, которые находятся за пределами системы.

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

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

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

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

В этом состоит проблема, потому что PCI-устройства могут быть перепрограммированы. Многие из этих вещей имеют записываемую прошивку, которую вы можете просто перепрошить на что-то враждебное. И это может поставить под угрозу безопасность всей операционной системы, так как позволит осуществить любую форму атаки, даже модифицировать саму ОС или извлечь ключ напрямую. В компьютерной криминалистике существует оборудование, предназначенное для таких вещей в процессе расследования преступлений: они подключают что-то к вашему компьютеру и вытаскивают содержимое памяти. Вы можете сделать это с помощью FireWire, ExpressСard или Thunderbolt… Фактически всё это внешние порты, обеспечивающие доступ к внутренней системной шине.

Итак, было бы неплохо, если бы можно было не хранить ключ в RAM, потому что мы вроде как продемонстрировали, что оперативная память не очень надежна с точки зрения безопасности. Существует ли какое-нибудь выделенное хранилище ключей или специальное криптографическое оборудование? Да, существует. Можно использовать криптографические ускорители для веб-сервера, чтобы обрабатывать больше SSL-транзакций в секунду. Они устойчивы к несанкционированному вмешательству. У центров сертификации СА есть вещи, которые хранят их совершенно секретные ключи, но на самом деле они не предназначены для таких высокопроизводительных операций, как использование шифрования диска. Итак, есть ли другие варианты?

Можем ли мы использовать процессор как своего рода псевдоаппаратный крипто-модуль? Можем ли мы вычислить что-то вроде симметричного алгоритма блочного шифрования AES в ЦП, используя вместо RAM только нечто типа регистров ЦП?

Intel и AMD добавили в процессоры отличные новые инструкции, которые взяли на себя работу выполнять AES, так что теперь вы можете выполнять операции примитивного блочного шифрования всего лишь с одной простой инструкцией по сборке. Вопрос состоит в том, можем ли мы оставить наш ключ в памяти или мы можем выполнить этот процесс, не полагаясь на основную память? В современных процессорах x86 есть довольно большой набор регистров, и если кто-то из вас действительно пытался сложить все биты, которые имеются в этих, получается около 4 килобайт. Таким образом, мы можем фактически использовать некоторые CPU для хранения ключей и создания пространства для выполнения операций шифрования.

Одна из возможностей — использование аппаратных регистров отладки точек остановок. В типичном процессоре Intel имеется 4 таких регистра, и в системе x64 каждый из них будет содержать 64-битный указатель. Это 256 бит потенциального дискового пространства, которое большинство людей никогда не будет использовать. Конечно, преимущество использования регистров отладки состоит в их привилегированности, потому что доступ к ним может получить только операционная система. Здесь есть и другие приятные преимущества, например, при отключении питания процессора при выключении системы или переходе её в спящий режим вы действительно потеряете все содержимое регистра, так что можете не бояться «холодной перезагрузки».

Парень из Германии, Тило Мюллер, реализовал эту подобную штуку под названием TRESOR для Linux в 2011 году. Он протестировал производительность такой системы и сделал вывод, что она работает ничуть не медленнее, чем при регулярном вычислении AES программным обеспечением.
Как насчет того, чтобы вместо одного ключа хранить два 128-битных ключа? Это приведёт нас к большему пространству криптомодуля. Мы можем хранить один мастер-ключ, который никогда не покидает процессор при загрузке, а затем загружать и выгружать версии ключей, необходимые нам для совершения дополнительных операций и решения дополнительных задач.

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

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

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

Опять таки, наш друг из Германии, Тило Мюллер, реализовал версию TRESOR на микробитном визоре под названием BitVisor, который это делает. Это позволяет запускать отдельную операционную систему и прозрачно осуществляет доступ к шифрованию диска, и здорово то, что вам не нужно об этом заботиться или что-либо об этом знать. Доступ к диску полностью прозрачен для ОС, операционная система не может получить доступ к регистрам отладки, и IOMMU настроен таким образом, что гипервизор полностью защищен от каких-либо манипуляций.

Но, как оказалось, в памяти, кроме ключей шифрования диска, существуют и другие вещи, о которых стоит позаботиться. Есть проблема, которую я уже упоминал – раньше мы использовали шифрование контейнеров, а теперь преимущественно выполняем полное шифрование диска.
Мы делаем полное шифрование диска, потому что очень трудно убедиться в том, что вы случайно не запишите ваши конфиденциальные данные во временные файлы или системный кеш в процессе шифрования контейнера. Теперь, когда мы оцениваем RAM как небезопасное, ненадежное место для хранения данных, мы должны относиться к ней соответственным образом. Мы должны зашифровать все данные, утечка которых нежелательна, всё действительно важное, такое, как SSH-ключи, приватные ключи или PGP-ключи, даже менеджер паролей и любые документы категории «совершенно секретно», с которыми вы работаете.

У меня была очень глупая идея: можем ли мы зашифровать RAM? Или, по крайней мере, большую часть основной памяти, в которой будем хранить секреты, чтобы минимизировать возможный объём утечки.

И, повторюсь, удивительно это или нет, но ответ – да, можем! В доказательство этой концепции в 2010 году парень по имени Питер Петерсон попытался реализовать решение шифрования оперативной памяти RAM. Фактически шифруется не вся оперативная память — он разделил основную память на две части: небольшой незашифрованный компонент фиксированного размера под названием “clear”, и более крупное псевдоустройство подкачки, где все данные зашифровывались перед тем, как сохраниться в основной памяти. При этом синтетические тесты показали снижение производительности системы в 10-50 раз. Однако в реальном мире, когда вы запускаете, например, тест веб-браузера, он на самом деле работает довольно хорошо – всего лишь на 10% медленнее. Я думаю, с этим можно примириться. Проблема с этим доказательством реализации концепции заключалась в том, что он хранил ключ для расшифровки в основной памяти, потому что куда еще его можно было поместить? Автор рассматривал возможность использования таких вещей, как TPM для массовых операций шифрования, но эти вещи работали даже медленнее, чем выделенная аппаратная криптосистема, поэтому были полностью непригодны для использования.

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

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

Таким образом, одной из важнейших тем является возможность проверки целостности наших компьютеров. Пользователь должен быть в состоянии проверить, что компьютер не был взломан, прежде чем на нём аутентифицироваться. Для этого мы можем использовать инструмент — доверенный платформенный модуль Trusted Platform Module – реализацию спецификации, описывающей криптопроцессор. Это своего рода плохой рэп, позже мы подробнее поговорим об этом, но у него есть возможность измерить последовательность загрузки несколькими различными способами, чтобы позволить вам контролировать, какие данные TPM будут предоставлены конкретным состояниям конфигурации системы. Таким образом, вы сможете «запечатать» данные в конкретной конфигурации программного обеспечения, которое вы запускаете в системе. Есть несколько различных подходов к реализации этого, и есть замысловатая криптография, затрудняющая обход этой реализации. Так что, возможно, это реально сделать.

Что же такое ТРМ? Изначально это было своего рода грандиозное решение для управления цифровыми правами медиа-компаний. Медиа-компании могут удаленно проверить, что ваша система работает в какой-то «одобренной» конфигурации, прежде чем они позволят вам запустить программное обеспечение и разблокировать ключ доступа к видеофайлам. В действительности это оказалось весьма неудачным решением, поэтому никто даже не пытается практически использовать его для этих целей.

Я думаю, что лучший способ реализации такой идеи – это смарт-карта, которая закреплена на вашей материнской плате. Она может выполнять некоторые криптографические операции, RSA/SHA1, имеет генератор случайных чисел, и у нее есть контрмеры против физических атак, во время которых кто-то пытается получить доступ к хранящимся на ней данным. Единственная реальная разница между ТРМ и смарт-картой заключается в том, что она имеет возможность измерять состояние загрузки системы в регистры конфигурации платформы и обычно это отдельный чип на материнской плате. Таким образом, она может оказать положительное влияние на обеспечение безопасности.

23:10 мин

DEFCON 21. Одних паролей недостаточно, или почему «ломается» шифрование диска и как это можно исправить. Часть 2

Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас оформив заказ или порекомендовав знакомым, 30% скидка для пользователей Хабра на уникальный аналог entry-level серверов, который был придуман нами для Вас: Вся правда о VPS (KVM) E5-2650 v4 (6 Cores) 10GB DDR4 240GB SSD 1Gbps от $20 или как правильно делить сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).

VPS (KVM) E5-2650 v4 (6 Cores) 10GB DDR4 240GB SSD 1Gbps до весны бесплатно при оплате на срок от полугода, заказать можно тут.

Dell R730xd в 2 раза дешевле? Только у нас 2 х Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 ТВ от $249 в Нидерландах и США! Читайте о том Как построить инфраструктуру корп. класса c применением серверов Dell R730xd Е5-2650 v4 стоимостью 9000 евро за копейки?

Let's block ads! (Why?)