The dangers of not looking ahead

    На первый взгляд, dynamic в C# — просто object с поддержкой машинерии компилятора. Но не совсем.

    Ядром времени выполнения является DLR (Dynamic Language Runtime) — подсистема/фреймворк для поддержки динамических языков программирования. Существует реализация под собственно C#, который идет в поставке с .NET, и отдельная для Iron-языков.

    Когда мы работаем с обобщениями (generics), то CLR имеет свои оптимизации на предмет специализации оных. В тот момент, когда CLR+DLR должны работать с generics вместе, поведение написанного кода может стать непредсказуемым.

    Preamble


    Для начала необходимо вспомнить как поддерживаются обобщения CLR'ом.
    Каждый generic-тип имеет свою реализацию, т.е. отсутствует type-erasure. Но для ссылочных типов среда использует тип System.__Canon для шаринга кода. Это необходимо не столько из-за очевидности (каждый объект — ссылка размером машинное слово), сколько для разрешения циклической зависимости между типами.

    Об этом я уже писал:
    Дело в том, что generic-типы могут содержать циклические зависимости от других типов, что чревато бесконечным созданием специализаций для кода. Например:
    Generics cyclomatic dependencies
    class GenericClassOne<T>
    {
        private T field;
    }
    
    class GenericClassTwo<U>
    {
        private GenericClassThree<GenericClassOne<U>> field
    }
    
    class GenericClassThree<S>
    {
        private GenericClassTwo<GenericClassOne<S>> field
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine((new GenericClassTwo<object>()).ToString());
            Console.Read();
        }
    }
    


    Однако этот код не упадет и выведет GenericClassTwo`1[System.Object].

    Type loader (он же загрузчик типов) сканирует каждый generic-тип на наличие циклической зависимости и присваивает очередность (т.н. LoadLevel для класса). Хотя все специализации для ref-types имеют System.__Canon как аргумент типа — это следствие, а не причина.

    Фазы загрузки (они же ClassLoadLevel):

    enum ClassLoadLevel
    {
        CLASS_LOAD_BEGIN,
        CLASS_LOAD_UNRESTOREDTYPEKEY,
        CLASS_LOAD_UNRESTORED,  
        CLASS_LOAD_APPROXPARENTS,
        CLASS_LOAD_EXACTPARENTS,
        CLASS_DEPENDENCIES_LOADED,
        CLASS_LOADED,
        CLASS_LOAD_LEVEL_FINAL = CLASS_LOADED,
    };
    


    Infinite loop


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

    Рассмотрим иерархию классов:
    NB: код реальный — из проекта structuremap, хоть и претерпевший к этому моменту изменения. Пример использовался во время моего выступления «Эффективное использование DLR».

    public class LambdaInstance<T> : LambdaInstance<T, T>
    {
    }
    
    public class LambdaInstance<T, TPluginType> 
        : ExpressedInstance<LambdaInstance<T, TPluginType>, T, TPluginType>
    {
    }
    
    public abstract class ExpressedInstance<T>
    {
    }
    
    public abstract class ExpressedInstance<T, TReturned, TPluginType> : ExpressedInstance<T>
    {
    }
    

    И непосредственно код:

    class Program
    {
        static LambdaInstance<object> ShouldThrowException(object argument)
        {
            throw new NotImplementedException();
        }
    
        static void Main(string[] args)
        {
            // будет ли брошено исключение?
            ShouldThrowException((dynamic)new object());
        }
    }
    

    Вопрос: будет ли брошено исключение?
    Ответ: нет. Метод ShouldThrowException никогда не завершится. И stackoverflow (переноса на сайт) не произойдет.

    Хм… Так в чем же дело? — спросите Вы.
    Все просто — LambdaInstance<object>. Рассмотрим иерархию классов еще раз.

    LambdaInstance<T> наследуется от LambdaInstance<T, TPluginType>, который в свою очередь от ExpressedInstance<LambdaInstance<T, TPluginType>, T, TPluginType>.

    Вложенное наследование заметили?

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

    Для выражения ShouldThrowException((dynamic)new object()); DLR должен проинспектировать участок кода/сигнатуру метода. В этом процессе встречается LambdaInstance<object> и код превращается в бесконечный цикл.

    Почему не крешится? DLR не использует рекурсию. Более того, потребление памяти растет (ибо создаются доп. метаданные), но не сильно.

    Epilog


    Может показаться, что dynamic, как таковой, является вещью опасной. В следующий раз мы рассмотрим пример, где его использование — правильно.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 13

      0
      Это перевод?
        +3
        нет, а почему так решили?
          0
          Видимо потому что большая часть статей с глубоким капанием — заморские.
            +2
            Я думаю всё проще: название и заголовки на английском написаны.
        +1
        Я правильно понял, что без динамика эта рекурсия будет отрезовлена и выбросится исключение?

        Выглядит страшно, фиг так сообразишь, чего он повис.
          0
          да, именно так
          0
          Спасибо за статью!
          Но у меня осталась пара вопросов:
          1. DLR и CLR используют один и тот же TypeLoader?
          2. Почему DLR не использует рекурсию? Это какое-то фундаментальное ограничение, или команда DLR планирует реализовать эту функциональность в будущем?
            +1
            1. TypeLoader один и тот же
            2. На самом деле это не ограничение. Нерекурсивный (почти) алгоритм, используемый в DLR дает одновременно и плюс, и минус в виде отсутствии переполнения стека. К сожалению, разработка непосредственно DLR идет неактивно и только в ветке IronPython. По данному вопросу новостей нет
              0
              А не подскажите где можно найти исходный код собственно самого TypeLoader'a?

              Я полазил по проекту coreclr и исходникам .NET'a, но нашел, только вот это:

              Но оно все как-то мало подходит под то, о чем вы говорите.

                +1
                Код загрузки типов в основном сосредоточен в секции vm. Несколько размазан, но конкретно по теме: загрузка generics. Вместо метода CheckInstantiationForRecursion из проекта Rotor (sscli20) код стал более ООП-шным и теперь используется граф (RecursionGraph).

                кстати, по архитектуре загрузчика типов более подробно.
            0
            Здравствуйте. Спасибо за интересную статью.
            А не могли бы вы поподробнее объяснить вот эти слова:
            Type loader (он же загрузчик типов) сканирует каждый generic-тип на наличие циклической зависимости и присваивает очередность (т.н. LoadLevel для класса). Хотя все специализации для ref-types имеют System.__Canon как аргумент типа — это следствие, а не причина.
            Когда я писал кодогенерацию на Mono.Cecil с использованием generic'ов — ни с чем таким не сталкивался.
              +2
              Тип System.__Canon никогда не виден ни при написании MSIL'a, ни при приведении типов. Данная особенность является внутренней оптимизацией CLR и может не применяться др. реализациями. так это позволяет упростить сам процесс выявления зависимостей между типами загрузчиком, т.к. происходит трекинг лишь System.__Canon.

              p.s.
              Это никак не влияет на кодогенерацию MSIL будь то Mono.Cecil, либо Reflection.Emit и т.п.
            • UFO just landed and posted this here

              Only users with full accounts can post comments. Log in, please.