Введение.
В этой статье я бы хотел рассказать о своем хобби проекте под названием StbSharp.
Итак, в 2016 году мне пришла в голову весьма банальная идея - сделать собственный игровой кросс-платформенный движок на C#. И я озаботился поиском кросс-платформенной же библиотеки для загрузки картинок. Внезапно выяснилось, что подходящей просто не существовало. Было множество платформо-зависимых решений(напр. System.Drawing). А так же имелась SixLabors.ImageSharp. Но она была в состоянии ранней альфы. Мне же хотелось работать с решением, проверенным временем. Так я пришёл к идее портировать stb_image.h (очень популярной в геймдеве single-header библиотеки для загрузки картинок) на C#.
"А разве не легче было написать биндинги для нативной библиотеки? Хоть для той же stb_image?",- задаст справедливый вопрос читатель. Да, легче. И правильнее. О чём, собственно, и говорит заголовок этой статьи. Конечно, использование биндингов доставляет некоторые неудобства в плане того, что необходимо доставить соответствующий нативный бинарник на устройство конечного пользователя. Однако эти неудобства с лихвой окупаются достоинствами. А именно лучшим перформансом и портируемостью.
Однако, проект показался мне столь интересным, что я проигнорировал эти справедливые возражения.
Как шло портирование
Перво-наперво я героически переименовал stb_image.h в StbImage.cs и попробовал внести нужные правки в студии с помощью замены. Разумеется, эта затея с треском провалилась.
Не менее провальным был и второй подход. Я написал небольшой скрипт, где пытался добиться той же цели с помощью регулярных выражений. Результат оказался ненамного лучше: скрипт правил в одном месте, но ломал в другом.
Наконец я придумал рабочее решение, которые превратилось в отдельный проект под названием Sichem. Его идея заключалась в том, чтобы построить синтаксическое дерево исходного файла с помощью libclang, а затем обойти построенное дерево и сгенерировать C# код.
Sichem был написан на C# и использовал ClangSharp(биндинги для libclang). Причём мне пришлось сделать для него небольшое расширение под названием SealangSharp, поскольку ClangSharp того времени не умел делать ряд вещей, вроде определения типа оператора.
Таким образом, базовая версия Sichem заняла один-два месяца. Наконец он стал более ни менее рабочим и даже умудрился переварить stb_image.h. Сгенерированный C# код был ужасен и содержал бесчисленное количество синтаксических ошибок. Однако их - в большинстве случаев - можно было поправить с помощью обычного string.Replace. Более сложные ошибки приходилось править ручками.
К примеру, много боли принесло портирование указателей на функции. В C# они становились делегатами. Из-за чего структуры их содержащие становились managed. А, значит, с ними уже не работала арифметика указателей(которая в C# поддерживается в unsafe режиме). И соответствующий код приходилось переписывать.
Однако со временем ошибки были исправлены и StbImageSharp наконец стал компилироваться. Работать, впрочем, он не стал. Первые попытки загрузить картинку генерировали самые разные роды исключений. Впрочем, исправить их было несложно. Но даже после этих правок картинки загружаться не хотели. StbImageSharp давал на выход нечто соответствующее оригинальной картинке лишь в размерах.
Передо мною встала очередная задача - исправить несоответствия между работой оригинального stb_image.h и StbImageSharp. Я запускал одновременно две студии. В одной дебажил загрузку картинки через stb_image.h, а в другой - через StbImageSharp. Шаг за шагом я находил расхождения и исправлял. Наконец картинка успешно загрузилась.
Примерно тогда же мне в голову пришла идея автоматического тестера. Которая заключалась в том, чтобы пробежаться по всем картинкам в заданной директории, загрузить каждую через stb_image.h и StbImageSharp. А затем убедиться, что результаты совпадают с точностью до байта.
Тестер был написан и натравлен на коллекцию из примерно 800 картинок. Ещё немалое времени ушло на исправление вновь открывшихся ошибок. И после этого базовая версия StbImageSharp была готова.
Внедрение в MonoGame
После этого я преступил к пиару. А именно создал тему с кратким описанием проекта на форуме MonoGame.
Для тех кто не в курсе, MonoGame - это open-source реализация XNA. Т.е. игровой фреймворк на C#. Он поддерживает множество игровых платформ. Для каждой платформы у него своя сборка, которая использовала платформенное-зависимый способ загрузки картинок.
К примеру, MonoGame.Framework.DesktopGL загружал картинки через System.Drawing. MonoGame.Framework.WindowsDX - через DirectX. И т.д.
Разработчики MonoGame давно уже обсуждали возможность перехода на платформо-независимое решение. Они не хотели переходить на SixLabors.ImageSharp, поскольку он был слишком избыточным для их нужд. Кроме того они не хотели добавлять в проект дополнительную зависимость. StbImageSharp же был хорош тем, что позволял включить себя в виде исходного кода. Поэтому они заинтересовались проектом. Меня спросили о том, насколько производительным он получился.
Поэтому я доработал автоматический тестер так, чтобы он ещё и измерял производительность. И наконец узнал ответ на самый популярный вопрос, связанный с StbImageSharpом. А именно, что StbImageSharp работает примерно на 25% медленнее, чем stb_image.h.
Производительность оказалась приемлемой, поэтому меня попросили сделать PR с заменой загрузки картинок через System.Drawing на StbImageSharp для сборки MonoGame.Framework.DesktopGL. Планировалось для начала внедрить StbImageSharp только в эту сборку. И если она покажет себя хорошо, то в дальнейшем внедрить и в остальные открытые сборки(у MonoGame есть ещё и ряд закрытых сборок для консолей).
Таким образом, MonoGame 3.7 вышла с StbImageSharp для сборки DesktopGL. Ей начали пользоваться множество людей для реальных проектов. Что вскрыло новые баги. Например, у StbImageSharp возникали проблемы при асинхронной загрузке картинок. Баги оперативно правились. И, в целом, проект достойно показал себя. Поэтому в MonoGame 3.8 он был внедрён во все открытые сборки. И остаётся там по сей день.
Использование в Unity3D
Как известно у великого и ужасного Unity3D функция Texture2d.LoadImage обладает целым рядом проблем:
Она позволяет загружать картинки только в форматах Jpg и Png.
Она даёт на выход текстуру, а не картинку в обычной памяти. Что делает сложным внесение в неё изменений(нужно вначале выгрузить текстуру в обычную память, внести изменения, а потом загрузить результат назад в видеопамять).
Её можно запускать только в основном треде.
Поэтому не удивительно, что некоторые разработчики искали альтернативные способы загрузки картинок в ран-тайме и находили StbImageSharp.
По крайней мере, именно так поступил проект TriLib.
А другой разработчик, посмотрев на TriLib, создал целую библиотеку, внедряющую StbImageSharp в Unity3D: https://github.com/mochi-neko/StbImageSharpForUnity
Причём он написал статью, где подробно обосновал своё решение: https://synamon.hatenablog.com/entry/unity_image_loading
Статья на японском, но через переводчик можно понять о чём идёт речь.
Остальные порты
Из вышенаписанного может сложиться, что на C# была портирована только stb_image.h. На самом деле, библиотек stb было портировано куда больше(полный список можно посмотреть, если проследовать ссылке в начале статьи).
Наибольшее внимание уделялось и уделяется StbImageSharp, StbImageWriteSharp и StbTrueTypeSharp. Остальным внимание уделяется постольку-поскольку.
Следует отметить, что StbImageWriteSharp так же был внедрен в MonoGame. А благодаря StbTrueTypeSharp на свет появился другой весьма популярный в среде MonoGame/FNA проект под названием FontStashSharp.
На базе StbImageSharp была создана его safe версия - SafeStbImageSharp. Который использовал специальный класс FakePtr для симуляции арифметики указателей.
А SafeStbImageSharp - в свою очередь - породил StbImageJava, проект настолько непопулярный, что порой даже я забываю про его существование.
Впрочем, его непопулярность объясняется тем, что Java из коробки умеет загружать картинки. Хотя, вроде бы, её Image API не работает на андроиде.
Кроме того, я - будучи 100% C# программистом - так и не разобрался с тем как правильно работать с Javaским аналогом nuget.org и куда нужно закачивать пакеты. Поэтому на данный момент проект доступен только в виде исходного кода.
Так же, StbImageSharp породил StbImageBeef - порт stb_image.h на язык Beef. У проекта появился как минимум один активный пользователь, который запилил уже 3 PR. Я думал портануть на этот язык и несколько других библиотек stb. Однако выяснилось, что подобный проект уже существует. Впрочем, он выглядит не слишком активным. Поэтому возможно я вернусь к своей затее.
Наконец, последние порты были сделаны на язык Rust: https://github.com/StbRust
Стоит отметить, что они так же являются ненужными. Поскольку у раста давно есть как библиотека работы с картинками https://docs.rs/image/latest/image/, так и с загрузкой фонтов https://docs.rs/truetype/latest/truetype/
Однако, я их сделал чтобы проверить возможности своей новой утилиты по портированию С кода под названием Hebron. Которая является наследником Sichem. Но в отличии от него, умеет портировать не только на C#.
Эпилог
Данная статья поведала об истории StbSharp. Проекте, которых не хватал звёзд с неба, но всё же нашёл свою скромную нишу.
Комментарии (22)
svkozlov
06.09.2022 09:41сделать собственный игровой кросс-платформенный движок на C#
Хотелось движок, а сделали библиотеку? Может не правильно выразились?
rds1983 Автор
06.09.2022 15:39Хотел сделать движок. Стал искать библиотеку для загрузки картинок. Нужной не нашёл. Поэтому решил портануть stb_image.h
vanyas
06.09.2022 10:01Вопрос немного не по теме, но перейдя по ссылке на stb_image.h я увидел что там в заголовочном *.h файле лежит код, а не только заголовки. Я на си давно не писал, поясните пожалуйста, почему так?
rds1983 Автор
06.09.2022 15:42Это называется single-header library. Данный подход позволяет добавить всю библиотеку в проект через один #include.
Более подробно вопрос освещён в faq stb(см. вопрос How do I use these libraries?): https://github.com/nothings/stbDungeonLords
06.09.2022 21:30"single-header library"
Есть кстати ещё подход амальгамейшн (Amalgamation).
Gigatrop
06.09.2022 17:22Отличная библиотека и очень нужная. Почему-то для C# в нашем веку простой png и jpg ещё может являться проблемой. Я искал то, что можно просто взять и использовать, и не тащить какие-то комбайны, и ваша библиотека вне конкуренции.
AirLight
06.09.2022 21:10А какое назначение библиотеки? Из описания я не понял. Написано что позволяет загружать изображения - загружать куда, зачем? Какие кейсы использования?
rds1983 Автор
06.09.2022 21:39+1Самый очевидный кейс - геймдев. Там постоянно нужно загружать картинки.
AirLight
07.09.2022 03:29Куда загружать-то? Вы имеете ввиду считывать с диска?
rds1983 Автор
07.09.2022 03:31+1Под загружать имеется ввиду декодировать из форматов JPG, PNG, BMP, TGA, PSD, GIF и HDR.
Советую, кстати, посмотреть список dependents: https://github.com/StbSharp/StbImageSharp/network/dependents?package_id=UGFja2FnZS00NzgyMTc1NTY%3D
Там очень много самодельных игровых движков. Именно среди таких проектов StbImageSharp пользуется популярностью. Что, впрочем, совершенно логично.
catana
06.09.2022 21:39Было бы любопытно узнать сравнение производительности StbSharp и LXUI (https://habr.com/ru/post/671598/)
rds1983 Автор
06.09.2022 21:40+3Насколько я понял, LXUI использует именно StbSharp для загрузки картинок :)
http://lxui.ru/documentation/namespace_stb_image_sharp.html
hbn3
06.09.2022 21:41Интересно можно ли улучшить производительность оптимизировав код сгенерированной библиотеки, использовав специфичные .net методы?
В целом очень интересный проект. Я в свое время писал конвертор VB в C#, было конечно в разы проще, но по некоторым граблям тоже пришлось походить.rds1983 Автор
06.09.2022 21:43Конечно, можно.
Существует даже соответствующий форк: https://github.com/TechPizzaDev/StbSharp.Image
hbn3
06.09.2022 21:48Не в курсе сколько получилось выжать по сравнению с оригиналом? Посмотрел на их гитхаб, но цифр не нашёл.
Ну и интересно, есть ли оптимизации которые можно применить автоматически, т.е. встроить в сам Hebron.rds1983 Автор
06.09.2022 22:13Не в курсе. Можно спросить у её разраба.
Наверно, можно. Но повышение производительности генерируемого кода не является высокоприоритетной задачей. Мне кажется, что если человеку так важна производительность, то лучше ему сделать биндинги к stb_image.h.
pennanth
Ваш "ненужные" проекты пользуются довольно большой популярностью! Да и рассказываете вы хорошо: моя область, к примеру, к играм и картинкам ну никак не соотносится, однако ж прочитал статью целиком.
Можете рассказать про Hebron побольше? Может, в отдельной статье?
rds1983 Автор
Hebron работает по тому же принципу, что и Sichem. Т.е. с помощью ClangSharp строится синтаксическое дерево входного файла на C. А затем оно подаётся на вход одному из двух кодогенераторов:
Кодогенератор Roslyn, который генерит C#.
Кодогенератор Rust, который генерит Rust.
В будущем я планирую добавить кодогенераторы для Beef и для Jai(надеюсь, он всё таки когда-нибудь выйдет в публичный доступ).
Отмечу, что у всех моих портов, проекты-генераторы лежат в соответствующих репозиториях.
К примеру, вот проект-генератор StbImageSharp: https://github.com/StbSharp/StbImageSharp/tree/master/generation
А, вот, stb_image_rust: https://github.com/StbRust/stb_image_rust/tree/master/generation
В принципе, могу описать какие отличия в синтаксисе между C и C# принесли наибольшее количество боли. И как они решались. Это потянет на отдельную статью.
strelkan
тема интересная, напишите пожалуйста.