Unity3D — один из самых перспективных и быстроразвивающихся игровых движков на текущий момент. Время от времени, в официальном репозитории появляются новые библиотеки и компоненты, многие из которых до недавнего времени были недоступны в виде исходников. К сожалению, команда разработчиков Unity3D предоставила на растерзание общественности не весь исходный код проекта, а только некоторые его компоненты, библиотеки и демки. В этой статье мы попробуем найти всевозможные ошибки и опечатки в них, используя для этого статический анализатор PVS-Studio.
Мы решили проверить все компоненты, библиотеки и демки, написанные на языке C#, чей исходный код предоставлен в официальном репозитории разработчиков Unity3D:
Было бы очень интересно взглянуть на исходники непосредственно ядра движка, но кроме разработчиков ни у кого такой возможности пока нет. Поэтому сегодня на нашем операционном столе лишь малая часть исходных кодов движка, которые мы можем проверить. Наиболее интересными проектами для нас являются: новая UI система, предназначенная для реализации более гибкого графического интерфейса относительно старого топорного GUI, и сетевая библиотека, которая верой и правдой нам служила до появления UNet.
Также не меньшего интереса заслуживает MemoryProfiler, как мощный и гибкий инструмент профилирования ресурсов и нагрузок.
Все предупреждения, выданные анализатором, можно разделить на 3 уровня:
Мы будем рассматривать только высокий и средний уровни.
В таблице ниже представлен список проверенных проектов и итоговый результат проверки по всем проектам. Столбцы «Название проекта» и «Количество строк кода», думаю, всем понятны и не должны вызывать вопросов, а вот назначение столбца «Срабатывания анализатора» стоит объяснить. Он содержит в себе информацию о количестве срабатываний анализатора. Позитивными срабатываниями считаются те, которые прямо или косвенно указывают на ошибки или опечатки в коде. Ложные срабатывания — ложные сообщения анализаторы, которые указывают на корректные участки кода, подозревая наличие в них ошибки или опечатки. Как уже и говорилось ранее — все срабатывания разделены на 3 уровня. Мы будем рассматривать только высокий и средний, так как низкий уровень, в основном, содержит информационные сообщения или маловероятные ошибки.
По итогам проверки 10 проектов было получено 16 предупреждений высокого уровня, 75% из которых верно указали на проблемные места в коде, и 18 срабатываний среднего уровня, 39% из которых верно указали на проблемные места. Качество кода следует признать высоким, так как анализатор находит в среднем только одну ошибку на 2000 строк кода. Это хороший результат.
Итак, со статистикой мы закончили. Теперь приступим к рассмотрению непосредственно обнаруженных ошибок и опечаток.
Ошибочное регулярное выражение
V3057 Invalid regular expression patern in constructor. Inspect the first argument. AssetBundleDemo ExecuteInternalMono.cs 48
При попытке создания экземпляра класса Regex с данным паттерном мы получим исключение System.ArgumentException с сообщением:
Это говорит о том, что данный паттерн является некорректным, и объект Regex не может быть создан с данным паттерном. По всей видимости, программист допустил ошибку при его проектировании.
Возможно обращение к объекту с нулевой ссылкой
V3080 Possible null dereference. Consider inspecting 't.staticFieldBytes'. MemoryProfiller CrawledDataUnpacker.cs 20
После проверки объекта на null происходит обращение к нему. При этом обращение происходит независимо от результата проверки. Это может привести к возникновению исключения NullReferenceException. Вероятнее всего программист планировал использовать оператор условного и &&, но вследствие опечатки используется оператор логического и &.
Обращение к объекту перед проверкой его на null
V3095 The 'uv2.gameObject' object was used before it was verified against null. Check lines: 1719, 1731. UnityEngine.Networking NetworkServer.cs 1719
Сначала происходит обращение к объекту, и только потом его проверка на null. Вероятнее всего, если ссылка на объект окажется равной null, то мы получим исключение NullReferenceException, так и не дойдя до проверки.
Помимо приведенной выше ошибки, анализатор обнаружил еще 2 аналогичных срабатывания, которые при определенных обстоятельствах могут вызвать исключения:
Ранее уже встречается оператор if с таким же условием, содержащий в then части безусловный оператор return
Очень интересное срабатывание, показывающее всю силу копипасты, классический пример опечатки.
V3021 There are two 'if' statements with identical conditional expressions. The first 'if' statement contains method return. This means that the second 'if' statement is senseless UnityEngine.UI StencilMaterial.cs 64
Вполне вероятно, программист скопировал часть кода и забыл изменить условия проверки после вставки.
Исходя из этой опечатки, можно сказать, что вторая проверка должна иметь вид:
Создание экземпляра класса исключения без дальнейшего использования
V3006 The object was created but it is not being used. The 'throw' keyword could be missing: throw new ApplicationException(FOO). AssetBundleDemo AssetBundleManager.cs 446
Создается класс ApplicationException, но никак не используется. Вероятнее всего, программист хотел выбросить исключение, но забыл добавить оператор throw перед созданным исключением.
Неиспользуемые аргументы при форматировании строки
Как известно, при форматировании строк количество выражений типа {N} должно соответствовать количеству передаваемых аргументов.
V3025 Incorrect format. A different number of format items is expected while calling 'WriteLine' function. Arguments not used: port. AssetBundleDemo AssetBundleServer.cs 59
В этом случае, судя по смыслу данного участка кода, программист, скорее всего, забыл убрать аргумент из первой строки. С технической точки зрения данная опечатка не является критической и не приведет появлению ошибки, но и в то же время не несет никакой смысловой нагрузки.
Цикл, который может превратиться в вечный при определенных условиях
V3032 Waiting on this expression is unreliable, as compiler may optimize some of the variables. Use volatile variable(s) or synchronization primitives to avoid this. AssetBundleDemo AssetBundleServer.cs 16
Вероятнее всего, программист хотел дождаться в цикле момента завершения внешнего процесса, но не учел, что переменная masterProcess может изначально принять значение null, если процесс не будет найден, что вызовет бесконечный цикл. Для корректной работы данного алгоритма необходимо запрашивать процесс по его идентификатору каждую итерацию цикла:
Небезопасная инициация события
Анализатор обнаружил потенциально небезопасный вызов обработчика события (event). Возможно возникновение исключения NullReferenceException.
V3083 Unsafe invocation of event 'unload', NullReferenceException is possible. Consider assigning event to a local variable before invoking it. AssetBundleDemo AssetBundleManager.cs 47
В данном участке кода происходит проверка на null поля unload, и затем происходит вызов данного события. Проверка на null позволит избежать исключения в случае, если на событие никто не подписан на момент его вызова.
Однако представим, что у события есть один подписчик. И в момент между проверкой на null и непосредственными вызовом обработчика события существует вероятность, что будет произведена отписка от события, например, в другом потоке. Чтобы обезопасить себя в данной ситуации можно сделать так:
Таким образом, проверка события на null и вызов его обработчика будут выполнены в виде одной команды, что обеспечит безопасный вызов события.
Часть логического выражения всегда истинна или ложна
V3063 A part of conditional expression is always false: connId < 0. UnityEngine.Networking ConnectionArray.cs 59
Выражение connId < 0 во второй проверке функции get всегда будет равно false, так как, используя это выражение в первой проверке, всегда производится выход из функции. Исходя из этого, во второй проверке это выражение не несет никакой смысловой и функциональной нагрузки.
Также была найдена еще одна похожая ошибка.
Думаю, не стоит говорить о том, что данное свойство может быть легко упрощено до вида:
Помимо представленных выше двух примеров, в проектах были найдены еще 6 аналогичных ошибок:
Как и в любых других проектах, здесь не обошлось без ошибок и опечаток. Как вы могли заметить, PVS-Studio наиболее преуспел в поиске опечаток.
В свою очередь, вы также можете проверить свой, или любой другой проект, написанный на языке C/C++/C#, с помощью данного статического анализатора.
Спасибо всем за внимание! Желаю вам безбажных программ.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ivan Kishchenko. Discussing Errors in Unity3D's Open-Source Components.
Введение
Мы решили проверить все компоненты, библиотеки и демки, написанные на языке C#, чей исходный код предоставлен в официальном репозитории разработчиков Unity3D:
- UI System — система для реализации графического интерфейса.
- Networking — система для реализации мультиплеера.
- MemoryProfiler — система профилирования используемых ресурсов.
- XcodeAPI — компонент для взаимодействия со средой разработки Xcode.
- PlayableGraphVisualizer — система визуализации процесса выполнения проекта.
- UnityTestTools — утилиты тестирования Unity3D (без Unit тестов).
- AssetBundleDemo — проект, содержащий исходники AssetBundleServer'а и демонстрирующий использование AssetBundle системы.
- AudioDemos — проекты, демонстрирующие использование аудио системы.
- NativeAudioPlugins — аудио плагины (нас интересует только код, демонстрирующий их использование).
- GraphicsDemos — проекты, демонстрирующие использование графической системы.
Было бы очень интересно взглянуть на исходники непосредственно ядра движка, но кроме разработчиков ни у кого такой возможности пока нет. Поэтому сегодня на нашем операционном столе лишь малая часть исходных кодов движка, которые мы можем проверить. Наиболее интересными проектами для нас являются: новая UI система, предназначенная для реализации более гибкого графического интерфейса относительно старого топорного GUI, и сетевая библиотека, которая верой и правдой нам служила до появления UNet.
Также не меньшего интереса заслуживает MemoryProfiler, как мощный и гибкий инструмент профилирования ресурсов и нагрузок.
Найденные ошибки и подозрительные места
Все предупреждения, выданные анализатором, можно разделить на 3 уровня:
- Высокий — наиболее вероятная ошибка.
- Средний — возможная ошибка или опечатка.
- Низкий — предупреждение о маловероятно возможной ошибке или опечатке.
Мы будем рассматривать только высокий и средний уровни.
В таблице ниже представлен список проверенных проектов и итоговый результат проверки по всем проектам. Столбцы «Название проекта» и «Количество строк кода», думаю, всем понятны и не должны вызывать вопросов, а вот назначение столбца «Срабатывания анализатора» стоит объяснить. Он содержит в себе информацию о количестве срабатываний анализатора. Позитивными срабатываниями считаются те, которые прямо или косвенно указывают на ошибки или опечатки в коде. Ложные срабатывания — ложные сообщения анализаторы, которые указывают на корректные участки кода, подозревая наличие в них ошибки или опечатки. Как уже и говорилось ранее — все срабатывания разделены на 3 уровня. Мы будем рассматривать только высокий и средний, так как низкий уровень, в основном, содержит информационные сообщения или маловероятные ошибки.
По итогам проверки 10 проектов было получено 16 предупреждений высокого уровня, 75% из которых верно указали на проблемные места в коде, и 18 срабатываний среднего уровня, 39% из которых верно указали на проблемные места. Качество кода следует признать высоким, так как анализатор находит в среднем только одну ошибку на 2000 строк кода. Это хороший результат.
Итак, со статистикой мы закончили. Теперь приступим к рассмотрению непосредственно обнаруженных ошибок и опечаток.
Ошибочное регулярное выражение
V3057 Invalid regular expression patern in constructor. Inspect the first argument. AssetBundleDemo ExecuteInternalMono.cs 48
private static readonly Regex UnsafeCharsWindows =
new Regex("[^A-Za-z0-9\\_\\-\\.\\:\\,\\/\\@\\\\]");
При попытке создания экземпляра класса Regex с данным паттерном мы получим исключение System.ArgumentException с сообщением:
parsing \"[^A-Za-z0-9\\_\\-\\.\\:\\,\\/\\@\\]\" — Unrecognized escape sequence '
Это говорит о том, что данный паттерн является некорректным, и объект Regex не может быть создан с данным паттерном. По всей видимости, программист допустил ошибку при его проектировании.
Возможно обращение к объекту с нулевой ссылкой
V3080 Possible null dereference. Consider inspecting 't.staticFieldBytes'. MemoryProfiller CrawledDataUnpacker.cs 20
.... = packedSnapshot.typeDescriptions.Where(t =>
t.staticFieldBytes != null & t.staticFieldBytes.Length > 0 // <=
)....
После проверки объекта на null происходит обращение к нему. При этом обращение происходит независимо от результата проверки. Это может привести к возникновению исключения NullReferenceException. Вероятнее всего программист планировал использовать оператор условного и &&, но вследствие опечатки используется оператор логического и &.
Обращение к объекту перед проверкой его на null
V3095 The 'uv2.gameObject' object was used before it was verified against null. Check lines: 1719, 1731. UnityEngine.Networking NetworkServer.cs 1719
if (uv2.gameObject.hideFlags == HideFlags.NotEditable ||
uv2.gameObject.hideFlags == HideFlags.HideAndDontSave)
continue;
....
if (uv2.gameObject == null)
continue;
Сначала происходит обращение к объекту, и только потом его проверка на null. Вероятнее всего, если ссылка на объект окажется равной null, то мы получим исключение NullReferenceException, так и не дойдя до проверки.
Помимо приведенной выше ошибки, анализатор обнаружил еще 2 аналогичных срабатывания, которые при определенных обстоятельствах могут вызвать исключения:
- V3095 The 'm_HorizontalScrollbarRect' object was used before it was verified against null. Check lines: 214, 220. UnityEngine.UI ScrollRect.cs 214
- V3095 The 'm_VerticalScrollbarRect' object was used before it was verified against null. Check lines: 215, 221. UnityEngine.UI ScrollRect.cs 215
Ранее уже встречается оператор if с таким же условием, содержащий в then части безусловный оператор return
Очень интересное срабатывание, показывающее всю силу копипасты, классический пример опечатки.
V3021 There are two 'if' statements with identical conditional expressions. The first 'if' statement contains method return. This means that the second 'if' statement is senseless UnityEngine.UI StencilMaterial.cs 64
if (!baseMat.HasProperty("_StencilReadMask"))
{
Debug.LogWarning(".... _StencilReadMask property", baseMat);
return baseMat;
}
if (!baseMat.HasProperty("_StencilReadMask")) // <=
{
Debug.LogWarning(".... _StencilWriteMask property", baseMat);
return baseMat;
}
Вполне вероятно, программист скопировал часть кода и забыл изменить условия проверки после вставки.
Исходя из этой опечатки, можно сказать, что вторая проверка должна иметь вид:
if (!baseMat.HasProperty("_StencilWriteMask"))
Создание экземпляра класса исключения без дальнейшего использования
V3006 The object was created but it is not being used. The 'throw' keyword could be missing: throw new ApplicationException(FOO). AssetBundleDemo AssetBundleManager.cs 446
if (bundleBaseDownloadingURL.ToLower().StartsWith("odr://"))
{
#if ENABLE_IOS_ON_DEMAND_RESOURCES
Log(LogType.Info, "Requesting bundle " + ....);
m_InProgressOperations.Add(
new AssetBundleDownloadFromODROperation(assetBundleName)
);
#else
new ApplicationException("Can't load bundle " + ....); // <=
#endif
}
Создается класс ApplicationException, но никак не используется. Вероятнее всего, программист хотел выбросить исключение, но забыл добавить оператор throw перед созданным исключением.
Неиспользуемые аргументы при форматировании строки
Как известно, при форматировании строк количество выражений типа {N} должно соответствовать количеству передаваемых аргументов.
V3025 Incorrect format. A different number of format items is expected while calling 'WriteLine' function. Arguments not used: port. AssetBundleDemo AssetBundleServer.cs 59
Console.WriteLine("Starting up asset bundle server.", port); // <=
Console.WriteLine("Port: {0}", port);
Console.WriteLine("Directory: {0}", basePath);
В этом случае, судя по смыслу данного участка кода, программист, скорее всего, забыл убрать аргумент из первой строки. С технической точки зрения данная опечатка не является критической и не приведет появлению ошибки, но и в то же время не несет никакой смысловой нагрузки.
Цикл, который может превратиться в вечный при определенных условиях
V3032 Waiting on this expression is unreliable, as compiler may optimize some of the variables. Use volatile variable(s) or synchronization primitives to avoid this. AssetBundleDemo AssetBundleServer.cs 16
Process masterProcess = Process.GetProcessById((int)processID);
while (masterProcess == null || !masterProcess.HasExited) // <=
{
Thread.Sleep(1000);
}
Вероятнее всего, программист хотел дождаться в цикле момента завершения внешнего процесса, но не учел, что переменная masterProcess может изначально принять значение null, если процесс не будет найден, что вызовет бесконечный цикл. Для корректной работы данного алгоритма необходимо запрашивать процесс по его идентификатору каждую итерацию цикла:
while (true) {
Process masterProcess = Process.GetProcessById((int)processID);
if (masterProcess == null || masterProcess.HasExited) // <=
break;
Thread.Sleep(1000);
}
Небезопасная инициация события
Анализатор обнаружил потенциально небезопасный вызов обработчика события (event). Возможно возникновение исключения NullReferenceException.
V3083 Unsafe invocation of event 'unload', NullReferenceException is possible. Consider assigning event to a local variable before invoking it. AssetBundleDemo AssetBundleManager.cs 47
internal void OnUnload()
{
m_AssetBundle.Unload(false);
if (unload != null)
unload(); // <=
}
В данном участке кода происходит проверка на null поля unload, и затем происходит вызов данного события. Проверка на null позволит избежать исключения в случае, если на событие никто не подписан на момент его вызова.
Однако представим, что у события есть один подписчик. И в момент между проверкой на null и непосредственными вызовом обработчика события существует вероятность, что будет произведена отписка от события, например, в другом потоке. Чтобы обезопасить себя в данной ситуации можно сделать так:
internal void OnUnload()
{
m_AssetBundle.Unload(false);
unload?.Invoke(); // <=
}
Таким образом, проверка события на null и вызов его обработчика будут выполнены в виде одной команды, что обеспечит безопасный вызов события.
Часть логического выражения всегда истинна или ложна
V3063 A part of conditional expression is always false: connId < 0. UnityEngine.Networking ConnectionArray.cs 59
public NetworkConnection Get(int connId)
{
if (connId < 0)
{
return m_LocalConnections[Mathf.Abs(connId) - 1];
}
if (connId < 0 || connId > m_Connections.Count) // <=
{
...
return null;
}
return m_Connections[connId];
}
Выражение connId < 0 во второй проверке функции get всегда будет равно false, так как, используя это выражение в первой проверке, всегда производится выход из функции. Исходя из этого, во второй проверке это выражение не несет никакой смысловой и функциональной нагрузки.
Также была найдена еще одна похожая ошибка.
public bool isServer
{
get
{
if (!m_IsServer)
{
return false;
}
return NetworkServer.active && m_IsServer; // <=
}
}
Думаю, не стоит говорить о том, что данное свойство может быть легко упрощено до вида:
public bool isServer
{
get
{
return m_IsServer && NetworkServer.active;
}
}
Помимо представленных выше двух примеров, в проектах были найдены еще 6 аналогичных ошибок:
- V3022 Expression 'm_Peers == null' is always false. UnityEngine.Networking NetworkMigrationManager.cs 710
- V3022 Expression 'uv2.gameObject == null' is always false. UnityEngine.Networking NetworkServer.cs 1731
- V3022 Expression 'newEnterTarget != null' is always true. UnityEngine.UI BaseInputModule.cs 147
- V3022 Expression 'pointerEvent.pointerDrag != null' is always false. UnityEngine.UI TouchInputModule.cs 227
- V3063 A part of conditional expression is always true: currentTest != null. UnityTestTools TestRunner.cs 237
- V3063 A part of conditional expression is always false: connId < 0. UnityEngine.Networking ConnectionArray.cs 86
Заключение
Как и в любых других проектах, здесь не обошлось без ошибок и опечаток. Как вы могли заметить, PVS-Studio наиболее преуспел в поиске опечаток.
В свою очередь, вы также можете проверить свой, или любой другой проект, написанный на языке C/C++/C#, с помощью данного статического анализатора.
Спасибо всем за внимание! Желаю вам безбажных программ.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ivan Kishchenko. Discussing Errors in Unity3D's Open-Source Components.
Прочитали статью и есть вопрос?
Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.
Поделиться с друзьями
Комментарии (74)
icepro
23.08.2016 23:37Странно что «Неиспользуемые аргументы при форматировании строки» позиционируется как средний или высокий уровень предупреждения.
Как по мне то тут ничего страшного нет. Падать не будет. Неправильного поведения тоже не ожидается.Andrey2008
24.08.2016 00:01+2Уровень у нас, это не только критичность ошибки, но и её достоверность. Конечно, это два совершенно разных понятия. Но приходится как-то объединять эти две разные характеристики в одну. Двухмерную шкалу измерения ошибки пользователи не оценят :). Здесь высокая достоверность, что это ошибка. Вот и первый уровень.
FrozenTwilight
24.08.2016 14:48Здравствуйте. Было бы интересно узнать как обстоят дела с кодом у OGRE 3D.
Nirvano
Там не по этой причине nullRef не упадет. Это код будет преобразован в сохранение поля в локальную переменную, последующей проверкой локальной переменной и её же вызов. Т.к. это локальная переменная то у нас есть гарантия что ее никто не поменяет после проверки, в отличии от поля.
mayorovp
Там все еще хуже. "Стандарт" C# не запрещает оптимизатору использовать вместо поля локальную переменную или наоборот когда он уверен, что там одинаковые значения — если только поле не объявлено как volatile. Слово "стандарт" я взял в кавычки — потому что полного стандарта у C# нет, особенно в части модели памяти, вместо этого есть куча постов в блогах у авторов языка.
Поэтому, формально даже код с явным "вытягиванием" в локальную переменную может работать некорректно!
Но, "по счастливой случайности", все известные реализации оптимизируют доступ к полям через локальные переменные, а не наоборот (иначе бы дырку быстро заткнули). По той же причине явно неправильный код может нормально работать:
Появление оператора безопасной навигации в этом плане — не только удобный синтаксический сахар — но и спасение от проблемы неоднозначности стандарта, ведь этот оператор действительно гарантирует вызов метода лишь при непустом значении левого выражения:
Tutanhomon
Возможно сорсы движка компилируются другой версией дотнета, но та версия, которая используется для компиляции исходников разработчика (собственно, завязанная на версию моно) еще не умеет такие конструкции.
mayorovp
Ну, можно компилировать компилятором, идущим в составе Microsoft Build Tools 2015 — главное же получить бинарник, дальше версия языка не имеет значения, как и версия рантайма. Ну или самому компилятор собрать — исходный код Roslyn, кажется, открыт.
И поскольку сам компилятор написан на C# — его можно попытаться запустить через тот же Mono...
Но это я так, в порядке шутки.
Tutanhomon
Мсье понимает (С) ))
Leopotam
Абсолютно неверно. Советую попробовать скомпилировать coroutine в рантайме юнити и в рантайме C# .net и посмотреть рефлектором / испаем — абсолютно разный код. По той же причине неправильно будут работать BeginInvoke и тп штуки если их скомпилить свежим рантаймом в dll и положить в юнити. Проблема — в патченном моно из юнити.
mayorovp
Но это же не означает, что они несовместимы!
Leopotam
Они чисто условно совместимы. Попробуйте скомпилировать нечто специфическое, async / await, Task-и или что-нибудь этакое из C# — по такой логике «оно должно работать, это же C#!» :) Всегда нужно учитывать рантайм, на котором будет работать байткод. + сейчас юнитеки везде пропихивают il2cpp (с 5.4 стал официальным рантаймом для android), что тоже может повлечь за собой неадекватное поведение кода, скомпилированного не в родном для юнити окружении.
Есть такая библиотечка — https://github.com/sta/websocket-sharp, реализующая вебсокеты поверх штатных сокетов. Ну и до юнити5 сокеты были отрублены. Ну как отрублены — их криворукие индусы в билд-процедуре проверяли типы в клиентской сборке — не начинаются ли они с System.Net.Sockets и прерывать процесс с эксепшном «насяльника, купите прошку, кушать-ма ощинь хоцца». И тут же проверяли whitelist сборок, в которых разрешены были эти типы. Теперь уже можно рассказать очень простой способ обхода этого запрета — просто берешь и делаешь неймспейс, начинающийся с Systemxxx + даешь имя сборки с таким же именем (у меня была SystematicLaziness.dll :) ) и тест проходит, те в такой сборке можно было гонять сокеты без проблем. Вот берешь эту MIT-овскую библиотечку, меняешь неймспейс, компилишь — все вроде работает в редакторе, а вот на реальных девайсах начинаются чудеса (особенно после AOT тогда еще на ios). Собственно, автор подхачил ее на совместимость с моно (проблема была в вызове асинхронных методов) и выложил на ассетстор — в реп фиксы не пошли, но можно было погуглить и найти решение.
mayorovp
Я компилил async / await для .NET 2.0 — сомневаюсь, что под измененный Mono это будет сложнее.
Leopotam
А можно привести код, полученный рефлектором / илспаем после такой генерации в dll? Потому что это малореально в рантайме 2.0 (если только через ThreadPool и кучу сахара).
mayorovp
Он ничем не отличается от кода под 4.5
Надо просто подсунуть нужные классы, не важно в какой сборке. Я их в то время писал сам — но сейчас советую просто вытащить нужные файлы из исходников Mono или CoreCLR
Leopotam
Еще раз — в юнити mono 2.6.3 до сих пор, ни о каких .net4.5 и даже .net4 разговора не идет. Соберите в честный .net2.0 — оно не заработает.
mayorovp
Заработает, если добавить в проект требуемые классы. Точно так же, как у меня все заработало в честном 2.0
Да, у меня в проекте был класс Task в пространстве имен System.Threading.Tasks. Ну и что?
Leopotam
https://drive.google.com/open?id=0BzXJzWlUcV9lOGF5V0FTc3gzdlk — вот все, что есть в моно.
Leopotam
+ небольшой довесок https://drive.google.com/open?id=0BzXJzWlUcV9lSDRkMXAwMXNDb00
mayorovp
Кто из нас слепой? https://github.com/mono/referencesource/tree/mono-4.4.0-branch/mscorlib/system/threading/Tasks
Разумеется, я не имел в виду вытащить класс из той же самой версии Mono, которая его не имеет! Его надо вытащить в виде исходного кода из той версии, в которой этот класс есть, и положить среди своих файлов.
Leopotam
Уверен, что не я. Апгрейд компилятора до моно4.4 только планируется в юнити5.5, когда будет — неизвестно, возможно перенесут на 5.6 и тд.
mayorovp
Да какая разница, какая там версия?! Вам что, религия не позволяет взять пару исходников из другой версии?
Leopotam
Зачем я буду это делать и обеспечивать себе будующий геморой с апдейтом, если текущий компилятор не поддерживает сахар с «async / await» в коде? Если без них, то можно все написать и поверх тредпула без лишних плясок.
mayorovp
Затем, что разговор исходно шел именно про возможность замены компилятора, на что вы возразили, что async/await не будет работать.
Так вот — будет, если подложить ему кучку файликов.
Leopotam
Не будет — компилятор делает подмену-сахар инструкций в какие-то системные классы, аналогично работает синтсаксис linq. Нельзя заставить текущий компилятор обрабатывать ключевые слова «async / await» в коде. Если писать код в VS с обновленным компилятором, то возникнет проблема с strong-name типами (Task и прочими) — они должны будут лежать в сборке именно с тем именем / неймспейсом и подписью, что и в VS. Про подпись не уверен (моно в юнити вроде наплевать на это), но с полным путем нужных типов будут проблемы.
mayorovp
Я это правда делал. С именем сборки никакой проблемы не возникло — компилятор его не проверяет. Совсем не проверяет.
Leopotam
Если использовалась внешняя сборка с подписью, то ее имя и паблик-кей еще вроде, не уверен, нужно проверять.
mayorovp
Использовалась кем? Вы вообще читаете что я пишу?
Откуда я в честном .NET 2.0, по-вашему, брал внешнюю сборку с подписью? Особенно учитывая, что вместо стандартных реализаций я в тот раз использовал свой велосипед?
mayorovp
PS могу даже привести список классов, которых достаточно чтобы компилятор смог скомпилировать любой код, использующий async/await:
System.Threading.Tasks.Task
System.Threading.Tasks.Task<>
System.Runtime.CompilerServices.AsyncVoidMethodBuilder
System.Runtime.CompilerServices.AsyncTaskMethodBuilder
System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>
Task.GetAwaiter()
Последние 4 типа можно сделать структурами для экономии памяти.
Разумеется, если копировать их из Mono — придется скопировать еще несколько, которые используются внутри. А из CoreCLR без изменений скопировать и вовсе не получится — там часть плясок вокруг ExecutionContext придется вырезать.
Leopotam
Потому что автор статьи даже не удосужился узнать особенности того, что тестирует. Конструкция "?.Invoke" появилась только в C#6, а в юнити до сих пор mono 2.6.3, что примерно соответствует C#3.5 + default-ы из C#4.
Ну и про «а вдруг оно поменяется из другого потока» — юнити в паблик-апи вся исключительно однопоточная + внутренние проверки на вызов только из основного потока. Если кто-то решил запилить свой код в другом потоке (а это он должен сделать специально), то пусть сам и обеспечивает целостность и непротиворечивость.
Еще автор статьи наверняка бы предложил «оптимизацию» конструкции «var t = a != null? a: b» в «var t = a ?? b», абсолютно не вникая в перегрузку операторов в кастомной реализации моно юнити (жаль, что не встретил, а то было бы совсем замечательно).
Вывод — меньше рекламы своего поделия, больше изучения предмета тестирования.
Andrey2008
Вникать в каждый проект у нас, к сожалению, нет никаких сил.
Leopotam
Прекратите тогда вводить людей в заблуждение разбором «ошибок», потому что каждый конкретный продукт может содержать особенности реализации, в которые вы не собираетесь вникать, но с удовольствием расскажите, что ваша тулза нашла ошибку. По сути вы делаете себе антирекламу подобными статьями и комментариями.
Lailore
Если что, если использовать Visual Stdudio и компилировать DLL который засовывается в Unity, то все прекрасно работает.
Leopotam
Почитайте еще раз, что я написал — простой линейный код почти всегда будет работать как надо, но не весь (те же coroutine-ы + всякие волшебные вещи типа принудительного boxing / unboxing enum-ключа Dictionary в foreach) + абсолютно другой код в плане размещения локальных переменных в теле циклов (приходится выносить руками чтобы не сильно падала скорость) и тд и тп.
mayorovp
А что там не так со скоростью?
Leopotam
Моно (который в юнити) плохо оптимизирует такое — не пытается выкидывать ненужное и не пытается выносить определения переменных за скоуп тела цикла. Если руками вынести — получаешь хорошее ускорение.
Leopotam
ну и с enum-ом в качестве ключа — вот такой код будет гадить в память на каждой итерации (специфично только для старых версий моно, в том числе и для юнити):
Leopotam
Ну и в дополнение: http://stackoverflow.com/questions/18718399/every-iteration-of-every-foreach-loop-generated-24-bytes-of-garbage-memory
mayorovp
По вашей ссылке нашел вот такое описание бага: http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php, где есть вот это уточнение:
Если в переводе на русский язык и коротко — то это баг компилятора, а не рантайма. Замена компилятора устраняет утечку памяти.
Leopotam
Проблема в том, что они не могут менять ни компилятор ни рантайм вот так просто — могут вылезти всякие новые штуки, в линке (см мой пост ниже) на будующий апгрейд в 5.5 об этом сказано. Ну и у них сейчас есть более важная задача — il2cpp, те по сути на версию моно уже наплевать, не наплевать на трансляцию из конкретного байткода в il2cpp.
mayorovp
"У них" — да. Но мне все больше нравится идея использовать другой компилятор для своего проекта, если когда-нибудь буду его делать.
Leopotam
Скоро это будет невозможно — il2cpp будет на всех платформах (десктоп оставили на десерт), т.е. моно в рантайме не будет в принципе.
mayorovp
Если il2cpp будет работать в соответствии со своим названием (преобразование в cpp любого il) — это не будет проблемой.
ZimM
Это с чего бы наплевать? Моно как рантайм никуда не денется, о чем Юнитеки неоднократно говорили. Исключение составляет лишь iOS, для которой обязательна возможность AOT-сборки в 64 бита, чего Mono не умеет.
Что меня больше напрягает — так это то, что если юнитеки повысят версию C# для рантайма — им придется долго и нудно допиливать фичи новых версий языка в il2cpp, да и то это будет неполноценно, никаких dynamic и прочих плюшек.
mayorovp
Ну с чего бы? Какой бы версии ни был компилятор — на выходе у него будет корректный IL. И если il2cpp оправдает свое название — то он этот самый IL "проглотит" независимо от того кто его нагенерил.
Тот же dynamic в конечном итоге сводится к рефлексии. Если il2cpp умеет рефлексию — сумеет и dynamic.
ZimM
Толку с IL, если его нечем будет правильно выполнить? IL2CPP это не только конвертер из IL в С++, а и полноценный рантайм, у него своя реализация потоков, маршалинга и большой части стандартных классов, даже банального String. Практически уверен, что Task юнитекам придется реализовывать также самостоятельно.
mayorovp
Task — это высокоуровневая абстракция, которая прекрасно реализуется в чистом управляемом коде. И у нее уже есть реализация в Mono, ничего самостоятельно делать не придется.
Leopotam
il2cpp на ios — основной вроде с 5.1, il2cpp на android — основной с 5.4, il2cpp на webgl (и дальше через emscripten) — основной (с 5.4 webplayer выпилен окончательно).
На моно остались только десктопы, но и они скорее всего будут переведены на il2cpp. Про то, что моно останется — юнитеки говорили только про редактор. Потому и наплевать, лишь бы il2cpp в поном объеме поддерживал трансляцию.
ZimM
В WebGL другого варианта, кроме IL2CPP, по сути, и нет — не портировать же Моно…
А вот десктопы совершенно точно будут поддерживать как il2cpp, так и Моно в качестве рантайма, и юнитеки это говорили. Пруф можно найти тут в комментах тут:
https://blogs.unity3d.com/ru/2014/05/20/the-future-of-scripting-in-unity/
Leopotam
Так по сути это и сделало его основным — годным для продакшна. еще пара минорных версий фиксов и моно думаю будут помечать как deprecated, чтобы не тянуть 2 рантайма под одну платформу.
ZimM
Это было бы так, если бы они были 100% взаимозаменяемы. У Моно достаточно преимуществ перед IL2CPP (см. коммент ниже), и при этом нет каких-либо значимых недостатков (по крайней мере, для меня).
Leopotam
У моно есть один существенный недостаток, который с 5.4 превратился в боль для определенной группы людей — это легкая реверсируемость кода. В случае il2cpp это просто невозможно и китайские товарищи теперь будут рыдать.
ZimM
Это плюс, да. Хотя, IL2CPP никоим образом не помешал сообществу отреверсить Pokemon GO практически полностью :)
Leopotam
так а там точно был il2cpp? не думаю что они прямо прыгнули на 5.4 за пару недель до релиза. Ну и от разбора контента il2cpp не защищает + гораздо проще снифать незащищенный трафик, чем разбирать приложение. Собственно, так и делают сейчас всяких ботов — просто эмуляцией трафика от клиента.
ZimM
Точно. Лично колупал apk) Да и они могли ведь вполне пользоваться экспериментальной версией от 5.3.
От реверсинга трафика это не спасает, конечно, но сообщество и логику работы разбирало, так что… Само собой, особо никакой защиты il2cpp не дает, просто усложняет все на порядки.
ZimM
Будь моя на то воля, лично я бы использовал Mono всюду, где это возможно, по трем причинам:
1) Меньше итоговый размер билда.
2) Сборка быстрее на порядок.
3) AOT-компиляция автоматически лишает крутых штук типа Expression и System.Reflection.Emit, полагающихся на JIT-компиляцию.
Leopotam
AOT / il2cpp билды на ios бегают быстрее на более дохлом железе, чем на более мощном, но под андроидом. Да, можно говорить о том, что платформа вся такая замечательная, оптимизированная и т.п., но и статическая типизация делает свое дело.
ZimM
Ну я и говорю о том, что выбор рантайма зависит от проекта. В навороченной игре с кучей контента и размером билда эдак 200 МБ, пожалуй, il2cpp подойдет. Если же я делаю онлайн-пятнашки, мне даром не нужна прибавка +20 МБ к билду, когда можно всю игру запихать в 15 МБ (минимальный размер apk в 5.4.0 c Моно — примерно 10 МБ).
Leopotam
+20 оно дает в случае fat build, моно в этом же режиме дает +10. Не сильно большая разница, размер все-равно увеличен до 100мб на GP, а в апсторе так вообще 2гб. Другое дело, что сейчас невозможно выпилить всякий мусор типа UnityEngine.UI, UnityEngine.Networking и тп муть, которая никуда не уперлась — это все едет в билд и так же транслируется в il2cpp.
ZimM
Технически, неиспользуемые сборки участвуют в трансляции, но по факту на выходе трансляции имеем только код, который реально используется в коде. По крайней мере, так должно быть по задумке юнитеков…
Mingun
А что можно перегрузить в этом выражении? Разве тернарный оператор перегружается? И зачем это может понадобится делать?
Leopotam
Тут проблема в «a != null»: https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
т.е. есть вот такой удобный кейс:
Вот оно будет работать совсем не так, как задумано в «классическом» поведении.
Mingun
Интересный подход, хотя, как я понял, используется только в редакторе. Так что по сути если бы анализатор указал на это место, то был бы прав. правда, это добавило бы странных глюков при отладке :) Интересно, а почему в Unity не могли переопределить и оператор
??
?Leopotam
Так тут проблема в том, что это сделали не для всех, а только для специфических для юнити объектов типа GameObject, Component и тп + перегрузили для них «operator bool». Для штатных простых типов оно работает как задумано, те со строками, например, такое прокатит. Я и говорю — это именно специфика конкретного продукта.
Mingun
Погодите. Перегрузили же не оператор приведения типа, а оператор сравнения. Это значит, что во время компиляции компилятор знает, что за объект он с null-ом сравнивает. Что мешает компилятору на этапе компиляции посмотреть, что слева от оператора
??
специальный тип и реализовать этот оператор корректно?Leopotam
Они скорее всего подхачили UnityEngine.Object и все наследники получили вот такое нестандартное поведение. Те внедрение на уровне компилятора было не сильно большое, а возможно и без внедрения обошлось (путем внутренней реализации методов этого типа при инициализации моно-рантайма) — кто знает, об этом могут рассказать только те, у кого есть полные исходники. Но в 5.5 они собираются проапгрейдить компилятор до моно 4.4, причем не полностью: https://docs.google.com/document/d/1vGzfB3gIU4AEYOjseJxjYytisFkwOKRfEzI5Db6_1vQ/edit
ZimM
Ничего не мешает, в теории. Вот только по факту оператор ?? игнорирует перегрузку операторов сравнения и сравнивает через Object.ReferenceEquals.
Mingun
Насколько я понял, перегрузка оператора сделана не средством языка C# (где возможно перегружать некоторые операторы), а хаком в компиляторе (иначе непонятно, что там делать в компиляторе, если сравнение и так можно перегрузить). Поэтому мне совершенно непонятно, что мешает сделать хак законченным и хакать также синтаксическую конструкцию
??
, у которых тип слева входит в некий магический список классов, использовать один алгоритм сравнения, а для всех прочих —Object.ReferenceEquals
.ZimM
Перегрузка сделана обычными средствами языка, в этом можно убедиться, открыв UnityEngine.Object декомпилятором.
Проблема не в перегрузке, а в том, что оператор ?? в С#, грубо говоря, не поддерживает перегрузку операторов. И Юнити тут ни при чем.
Nirvano
Все что вы написали — верно. Только я не понял что значит: «Там все еще хуже». Код в виде:
Развернется в:
IL_0001: ldsfld class [mscorlib]System.Action ThirtyNineEighty.Program::Event // считываем 1 раз
IL_0006: dup // дублируем значение
IL_0007: brtrue.s IL_000c
IL_0009: pop
IL_000a: br.s IL_0012
IL_000c: callvirt instance void [mscorlib]System.Action::Invoke()
IL_0011: nop
IL_0012: br.s IL_0014
IL_0014: ret
Что соответствует тому что я написал. За исключением того, что здесь нет дополнительной локальной переменной. А используется дублирование значения записанного на стек, после считывания поля. Но это уже детали, главное что поле считывается 1 раз, а дальше идет работа с локальным значением.
mayorovp
Это в байт-коде поле считывается всего 1 раз, но после преобразования в машинный код теоретически может получиться что угодно. Кроме того, то, что вы показали — это поведение текущей версии конкретного компилятора, я же говорил про спецификацию языка.
Leopotam
О чем вы вообще спорите? Статья-то о тестировании чего? Пишите код в юнити-проекте, потом забирайте сборки из Temp-а и проверяйте, потому что .net компилятор гораздо прогрессивнее и агрессивнее в плане оптимизации, чем древний патченный моно.
Nirvano
Верно, что спецификация не указывает что будет. Но тем не менее, я настою на том, что практически считывание будет происходить 1 раз и в машинном коде.
x86
Event?.Invoke();
01652D90 mov ecx,dword ptr ds:[400AE28h] // считываем 1 раз
01652D96 test ecx,ecx // проверка на налл
01652D98 jne 01652D9B
01652D9A ret
01652D9B mov eax,dword ptr [ecx+0Ch] // достаем копию
01652D9E mov ecx,dword ptr [ecx+4]
01652DA1 call eax
01652DA3 ret
x64
00007FFE92E44CE0 sub rsp,28h
00007FFE92E44CE4 mov rcx,21B1DE44C20h // считываем 1 раз
00007FFE92E44CEE mov rcx,qword ptr [rcx]
00007FFE92E44CF1 test rcx,rcx // проверка на налл
00007FFE92E44CF4 jne 00007FFE92E44CFB
00007FFE92E44CF6 add rsp,28h
00007FFE92E44CFA ret
00007FFE92E44CFB mov qword ptr [rsp+20h],rcx // достаем копию
00007FFE92E44D00 mov rcx,qword ptr [rcx+8]
00007FFE92E44D04 mov rax,qword ptr [rsp+20h]
00007FFE92E44D09 call qword ptr [rax+18h]
00007FFE92E44D0C nop
00007FFE92E44D0D add rsp,28h
00007FFE92E44D11 ret
И мне совершенно не понятно, почему Вы ко мне прицепились, ведь я всего лишь указал, что это выражение не будет преобразовано в 1 команду. И пояснил, что будет практически, а не теоретически.
mayorovp
Вот такие же "практики, а не теоретики" и попадаются теперь на постоянных UB в C++...