10 апреля 2010 в 10:36

Особенности работы CLR в .NET framework

.NET*
Начиная изучать язык C# и .NEt Framework я ни как не мог понять, как же работает CLR. Я либо находил огромные статьи, которые не осилить за 1 вечер либо слишком краткое, скорее даже запутывающее описание процесса (как в книге Г. Шилдта).
Некоторое время назад я решил, что было бы неплохо собирать знания, полученные из книг, «фичи» и часто используемые приемы в одном месте. А то новая информация быстро оседает в голове, но также быстро забывается и спустя несколько недель приходится вновь рыться в сотнях и тысячах строк текста, чтобы найти ответ на вопрос. Читая очередную книгу по программированию, я делал краткие пометки самого важного, что мне показалось. Иногда описывал некоторый процесс понятным мне языком с придуманным примером и т.д. Я не претендую на абсолютную правильность излагаемого материала. Это всего лишь мое понимание процесса, с моими примерами и информацией, которую я посчитал ключевой для понимания Проработав некоторый материал, я решил сохранить это для всех тех, кому это может быть полезно. А кто уже знаком — тот просто освежит это в памяти.

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

В качестве примера приведу текст программы, выводящий на экран возраст объекта:
исходный текст программы, чтобы было понятно:
using System;

namespace ConsoleApplication_Test_Csharp
{
  public class SomeClass
  {
    int age;
    public int GetAge()
    {
      age = 22;
      return age;
    }
  }
  public sealed class Program
  {       
    public static void Main()
    {
      System.Console.Write("My age is ");
      SomeClass me = new SomeClass();
      int myAge;
      myAge = me.GetAge();
      System.Console.WriteLine(myAge);
      Console.ReadLine();
    }
    
  }
}

* This source code was highlighted with Source Code Highlighter.


И так приступим:

Что такое CLR?


CLR (Common language runtime) — общеязыковая исполняющая среда. Она обеспечивает интеграцию языков и позволяет объектам благодаря стандартному набору типов и метаданным), созданным на одном языке, быть «равноправными гражданами» кода, написанного на другом.

Другими словами CLR этот тот самый механизм, который позволяет программе выполняться в нужном нам порядке, вызывая функции, управляя данными. И все это для разных языков (c#, VisualBasic, Fortran). Да, CLR действительно управляет процессом выполнения команд (машинного кода, если хотите) и решает, какой кусок кода (функцию) от куда взять и куда подставить прямо в момент работы программы. Процесс компиляции представлен на рисунке:
CLR

IL (Intermediate Language) — код на специальном языке, напоминающим ассемблер, но написанном для .NET. В него преобразуется код из других языков верхнего уровня (c#, VisualBasic). Вот тогда-то и пропадает зависимость от выбранного языка. Ведь все преобразуется в IL (правда тут есть оговорки соответствия общей языковой спецификации CLS, что не входит в рамки данной статьи)
Вот как он выглядит для функции SomeClass::GetAge()
IL

Компилятор, помимо ассемблера IL создает полные метаданные.

Метаданные — набор из таблиц данных, описывающих то, что определено в модуле. Также есть таблицы, указывающие на что ссылается управляемый модуль (например, импортируемые типы и числа). Они расширяют возможности таких технологий как библиотеки типов и файлы языка описания интерфейсов (IDL). Метаданные всегда связаны с файлом с IL кодом, фактически они встроены в *.exe или *.dll.
Таким образом метаданные это таблицы, в которых есть поля, говорящие о том, что такой-то метод находится в таком-то файле и принадлежит такому-то типу(классу).
Вот как выглядят метаданные для моего примера (таблицы метаданных просто преобразованы в понятный вид с помощью дизассемблера ILdasm.exe. На самом деле это часть *.exe файла программы:

metadata

TypeDef — это запись, для каждого типа, определенного в модуле
К примеру TypeDef #1 описывает класс SomeClass и показывает поле Field #1 с именем Field Name: age, метод MethodName: GetAge и конструктор MethodName: .ctor. Запись TypeDef #2 описывает класс Program.

Разобравшись с основными понятиями, давайте посмотрим из чего же состоит тот самый управляемый модуль (или просто наш файл ConsoleApplication_Test_Csharp.exe, который выполняет вывод на экран возраста объекта):

Заголовок показывает на каком типе процессора будет выполняться программа. РЕ32 (для 32 и 64 битных ОС) или РЕ32+ (только для 64 битных ОС)
Заголовок CLR — содержит информацию, превращающую этот модуль в управляемый (флаги, версия CLR, точки входа в Main())
Метаданные — 2 вида таблиц метаданных:
1) определенные в исходном коде типы и члены
2) типы и члены, имеющие ссылки в исходном коде.
Код IL — Код, создаваемый компилятором при компиляции кода на C#. Затем IL преобразуется в процессорные команды (0001 0011 1101… ) при помощи CLR (а точнее JIT)

Работа JIT



И так, что же происходит, когда запускается впервые программа?
Сперва происходит анализ заголовка, чтобы узнать какой процесс запустить (32 или 64 разрядный). Затем загружается выбранная версия файла MSCorEE.dll ( C:\Windows\System32\MSCorEE.dll для 32разрядных процессоров)
После чего вызывается метод, расположенный MSCorEE.dll, который и инициализирует CLR, сборки и точку входа функции Main() нашей программы.

static void Main()
{
  System.Console.WriteLine("Hello ");
  System.Console.WriteLine("Goodbye");
}

* This source code was highlighted with Source Code Highlighter.


Для выполнения какого-либо метода, например System.Console.WriteLine(«Hello „), IL должен быть преобразован в машинные команды (те самые нули и единицы) Этим занимается Jiter или just-in-time compiler.

Сперва, перед выполнением Main() среда CLR находит все объявленные типы (например тип Console).
Затем определяет методы, объединяя их в записи внутри единой “структуры» (по одному методу определенному в типе Console).
Записи содержат адреса, по которым можно найти реализации методов (т.е. те преобразования, которые выполняет метод).

Jit

При первом обращение к функции WriteLine вызывается JiT-compiler.
JiTer 'у известны вызываемый метод и тип, которым определен этот метод.
JiTer ищет в метаданных соответствующей сборки — реализацию кода метода (код реализации метода WriteLine(string str) ).
Затем, он проверяет и компилирует IL в машинный код (собственные команды), сохраняя его в динамической памяти.
После JIT Compiler возвращается к внутренней «структуре» данных типа (Console) и заменяет адрес вызываемого метода, на адрес блока памяти с исполняемыми процессорными командами.
После этого метод Main() обращается к методу WriteLine(string str) повторно. Т.к. код уже скомпилирован, обращение производится минуя JiT Compiler. Выполнив метод WriteLine(string str) управление возвращается методу Main().

Из описания следует, что «медленно» работает функция только в момент первого вызова, когда JIT переводит IL код в инструкции процессора. Во всех остальных случаях код уже находится в памяти и подставляется как оптимизированный для данного процессора. Однако если будет запущена еще одна программа в другом процессе, то Jiter будет вызван снова для того же метода. Для приложений выполняемых в х86 среде JIT генерируется 32-разрядные инструкции, в х64 или IA64 средах — соответственно 64-разрядные.

Оптимизация кода. Управляемый и неуправляемый код



IL может быть оптимизирован, т.е. из него будут удалены IL — команды NOP (пустая команда). Для этого при компиляции нужно добавить параметры

Debug версия собирается с параметрами: /optimize -, /debug: full
Release версия собирается с параметрами: /optimize +, /debug: pdbonly

Чем же отличается управляемый код от неуправляемого?

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

В управляемой среде компиляция производится в 2 этапа:

1) компилятор переводит C# код в IL
2) для исполнения нужно перевести IL код в машинный код процессора, что требует доп. динамической памяти и времени (как раз та самая работа JIT).

Взаимодействие с неуправляемым кодом:

— управляемый код может вызывать направляемую функцию из DLL посредствам P/Invoke (например CreateSemaphore из Kernel32.dll).
— управляемый код может использовать существующий COM-компонент (сервер).
— неуправляемый код может использовать управляемый тип (сервер). Можно реализовать COM — компоненты в управляемой среде и тогда не нужно вести подсчет ссылок интерфейсов.

Параметр /clr позволяет скомпилировать Visual С++ код в управляемые IL методы (кроме когда, содержащего команды с ассемблерными вставками ( __asm ), переменное число аргументов или встроенные процедуры ( __enable, _RetrurAddress )). Если этого сделать не получится, то код скомпилируется в стандартные х86 команды. Данные в случае IL кода не являются управляемыми (метаданные не создаются) и не отслеживаются сборщиком мусора (это касается С++ кода).

Система типов



В дополнение хочу рассказать о системе типов CTS, принятой Microsoft.

CTS (Common Type System) — общая система типов в CLR (тип, по-видимому — это аналог класса C#). Это — стандарт, признанный ECMA который описывает определение типов и их поведение. Также определяет правила наследования, виртуальных методов, времени жизни объектов. После регистрации ECMA стандарт получил название CLI ( Common Language Infrastructure)

— CTS поддерживает только единичное наследование (в отличие от С++)
— Все типы наследуются от System.Object (Object — имя типа, корень все остальных типов, System — пространство имен)

По спецификации CTS любой тип содержит 0 или более членов.

Основные члены:

Поле — переменная, часть состояния объекта. Идентифицируются по имени и типу.
Метод — функция, выполняющая действие над объектом. Имеет имя, сигнатуру(число параметров, последовательность, типы параметров, возвр. значение функции) и модификаторы.
Свойство — в реализации выглядит как метод (get/set) а для вызывающей стороны как поле ( = ). Свойства позволяют типу, в котором они реализованы, проверить входные параметры и состояние объекта.
Событие — обеспечивает механизм взаимного уведомления объектов.

Модификаторы доступа:

Public — метод доступен любому коду из любой сборки
Private — методы вызывается только внутри типа
Family (protected) — метод вызывается производными типами независимо от сборки
Assembly (internal) — метод вызывается любым кодом из той же сборки
Family or Assembly
(protected internal) — метод вызывается производными типами из любой сборки и + любыми типами из той же сборки.

CLS (Common Language Specification) — спецификации выпущенная Майкрософт. Она описывает минимальный набор возможностей, которые должны реализовать производители компиляторов, чтобы их продукты работали в CLR. CLR/CTS поддерживает больше возможностей, определенных CLS. Ассемблер IL поддерживает полный набор функций CLR/CTS. Языки (C#, Visual Basic) поддерживает часть возможностей CLR/CTS (в т.ч. минимум от CLS).
Пример на рисунке

CLS

Пример проверки на соответствие CLS

Атрибут [assembly: CLSCompliant(true)] заставляет компилятор обнаруживать любые доступные извне типы, содержащие конструкции, недопустимые в других языках.

  1. using System;
  2. [assembly: CLSCompliant(true)]
  3. namespace SomeLibrary
  4. {
  5.   // возникает предупреждение поскольку тип открытый
  6.   public sealed class SomeLibraryType
  7.   {
  8.     // тип, возвращаемый функцией не соответсвует CLS
  9.     public UInt32 Abc() { return 0; }
  10.  
  11.     // идентификатор abc() отличается от предыдущего, только если
  12.     // не выдерживается соответсвие
  13.     public void abc() { }    
  14.  
  15.     // ошибки нет, метод закрытый
  16.     private UInt32 ABC() { return 0; }
  17.  
  18.   }
  19.  
  20. }
* This source code was highlighted with Source Code Highlighter.


Первое предупреждение: UInt32 Abc() возвращает целочисленное целое без знака. Visaul Basic, например, не работает с такими значениями.
Второе предупрждение: два открытых метода Abc() и abc() — одиноквые и отличаются лишь регистром букв и возвращаемым типом. VisualBasic не может вызывать оба метода.

Убрав public и оставив только sealed class SomeLibraryType оба предупреждения исчезнут. Так как SomeLibraryType по-умолчанию будет internal и не будет виден извне сборки.

P.S. Статья основана на материалах из книги Дж. Рихтера «CLR via C#. Программирование на платформе Microsoft .NET Framework 2.0 на языке C#»
Артём @asArtem
карма
–111,0
рейтинг 0,0
Похожие публикации

Комментарии (54)

  • +5
    Хорошая, годная копипаста.
    • –1
      простите, что в понимаете под копипастом (я на хабре всего пару дней)?

      Текст проработан и написан мной, код тоже мой. В нем отражено мое понимание темы, ни как не претендующие на истину. Из книги здесь только 2 картинки.
      • +1
        Ну, у меня на столе лежит та самая книга Рихтера, и особой разницы не вижу.
        • –1
          Спасибо, но я не претендую на ОСОБУЮ разницу.
          Я в интернете не нашел ни чего подобного, что хоть как-то объясняло работу CLR и было на русском. Вот и решил написать то, как я это понимаю и вижу. Знания мои брались в том числе из Рихтера, я об этом написал в конце топика.
          Клонирования здесь нет. Есть переработанный материал.
          • +1
            Логично, потому что объяснениями работы CLR на русском занимается Рихтер. Например, здесь
            • –2
              может быть я оскорбил ваши чувства своим постом, но получить в первый же день в карму минус 1… Пропадает желание и энтузиазм писать и совершенствоваться.
              • 0
                Ну, допустим, не -1, а 0, и это нормально.

                Я то, как говорится, тут причем? Это все сообщество.
                • –3
                  ни кого не обвиняю.
                  да, теперь 0, но был -1 в карме. Кому-то спасибо за +
                  Ни как нельзя узнать кто + ставил?
                  • 0
                    Нельзя, тут все анонимно.
              • 0
                Вы поменьше о ней беспокойтесь, а то еще и возможность пропадет.
            • +2
              с каких пор Рихтер что-то объясняет на русском?

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

                Кто не читал — самое время почитать :)

                ( завтра студия 2010 и.нет 4 выходят вроде как )
                • 0
                  Кст, а на русском она будет? А то знание английского не позволяет комфортно (без словаря) читать данный труд :(
                • –3
                  почему часть? третья редакция книги
  • 0
    >>> Visaul Basic, например, не работает с такими значениями.
    >>> VB не может вызывать оба метода.

    2 хороших способа заставить-таки перестать писать на vb
  • 0
    Хорошая статья, тоже иногда собираю такие выписки только никуда их не публикую, в конце концов потом в этих выписках сам запутываешься.

    Вначале код немного не c#'арпный если мягко сказать, наверно писался ещё во времена 1.0. Вместо метода можно было использовать свойства и т.д.

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

    А если хочется написать, что то на unmanaget коде, делайте импорт.
    Как пример по моему, говорят хром написан наполовину на asm))
    • 0
      я писал чтобы понять можно было быстро во времена 4.0
      • 0
        Да мне всё нравится, даже забекапил в evernote статью, мало ли в черновики перенесёте.

        Тут просто не очень .net жалуют. Во всяком случае очень мало комментариев собирают, не знаю как насчёт рейтинга не интересуюсь этим.

        Пишите дальше, не обращайте внимания.
        • 0
          да я не гонюсь за рейтингом. Я в удовольствие собираю материал.
        • 0
          Здесь не только про .NET, но и вообще любые технические статьи не жалуют.
          • 0
            а какие жалуют?
            • +1
              О том, что сейчас находится в тренде, в «потоке», т.е. у всех на слуху — обзор нового iPad, обсуждение работы Почты, Яндекса, описание проблем с Internet Explorer.

              Можно взять самые обсуждаемые посты за последние несколько дней и написать свой на схожую тематику — это почти гарантирует хорошие оценки и интерес пользователей.
              • 0
                Спасибо, но я не хочу писать то, чем пока не интересуюсь. Пусть об этом пишут другие.
                • +2
                  Имхо тут имеет место быть некоторый сарказм со стороны JayDi (не в обиду ему будет сказано). На самом деле любая статья технического (или около-технического) характера, написанная человеком, который разбирается в теме, здесь приветствуется и оценивается высоко. Просто отдельные хаброжители воспринимают информацию из некоторых статей, как давно и повсеместено известную, отсюда их негативная реакция. В любом случае, спасибо вам за труд и высокой оценки хабровчан вашим будущим статьям!
  • 0
    Лучше бы кто-будь про управление памятью в .NET написал, с зарисовками и поподробнее, а то Рихтер тут не все хорошо разъяснил, на мой взгляд.
  • 0
    Мм, оптимизация кода в .NET заключается исключительно в удалении nop? А зачем их туда собственно ставить? оО
    • 0
      В целом они нужны для поддержки режима «редактирование и продолжение выполнения»
      Команды NOP позволяют ставить брейкпоинты на управляющих командах типа for, while, if.
      • 0
        А. ну вобщем-то, я так и подозревал.
        В принципе, логично.
        Но я ожидал что оптимизация несколько большее подразумевает :D
        • 0
          Я тоже раньше думал, что это на какие-то спец. команды меняет, но походу все и так подставляется сразу по ходу компиляции.
          • 0
            но там есть еще средства, я потом напишу про них. Когда побольше соберу инфы.
            в частности использование стека для хранения переменных повышает быстродействие, избавляя от копирования и сборки мусора
  • 0
    Спасибо. Очень интересная статья. Сам при изучении .net делаю краткий конспект по прочитанным темам, чтобы в случае чего не искать это в книге, а открыв свой блокнотик, быстренько посмотреть и продолжить писать программу.
    • 0
      Платформа .NET огромна… Всё в блокнотик не запишешь :( Хотя копипасты в, например, Гугл Нотпад, раньше делал регулярно.
      • 0
        так я записываю сейчас, по мере изучения, именно про сам c#. а всё из .net действительно не запишешь в блокнотик.
        • 0
          есть вещи, которые часто спрашивают на собеседовании, есть которые лучше помнить, чтобы не ляпнуть чего-то. А есть вещи, которые повышают навык написания «оптимального кода»
  • +1
    В общем-то статей по .NET от Microsoft на русском достаточно много в сети, а вот о наиболее популярной альтернативе — Mono, достаточно мало. Может кто посоветует ресурс? А то как — то не комфортно иногда делать проект как opensource на препроитарной платформе :)
  • +2
    Уточню децл про модификаторы доступа.

    .net доддерживает ещё один модификатор:

    protected private

    Его нет в C#, но он есть, например, в С++\CLI.

    Означает, что поле доступно наследнику в рамках одной assembly.
    То есть наследник из левой сборки обломается, наследник из той же сборки получит доступ.
    • +1
      Это модификатор internal && protected (в отличи от protected internal = internal || protected).
      • 0
        Ну если хочется прям академической точности))
        IL модификаторы доступа называются:

        famorassem — family or assembly — это protected internal в терминах C#
        famandassem — family and assembly — это protected private в терминах С++\CLI

        в терминах C# protected internal и internal protected значат ровно одно и то же — ваше утверждение что это два разных смысла — неверно.

        Получить famandassem модификатор доступа в C# нельзя никаким способом.
        • 0
          Опа, щас перечитал комент bobermaniac — всё он правильно сказал.
          Ложная тревога.
  • –1
    Стоит ли говорить, что компиляция во времени исполнения никак не ускоряет работу программы?
    • 0
      Компиляция во время исполнения производится всего 1 раз и не всех функций, а только тех, что были вызваны. Если это серверное приложение, к примеру, то компиляция 1 раз вообще не берется в расчет, зато что мы имеем? Мы имеем максимально оптимизированный и приспособленный код к условиям среды и платформы. Это дает огромную производительность. Думаю, что будущее именно за управляемым подходом к работе.
      • 0
        А если это десктопное приложение, пользователь бы наверно предпочел, чтобы компиляцией занимался разработчик, а не утяжелял программу компилятором.
        • 0
          Ну тем не менее большинство десктопных донетных приложений прекрасно устраивают пользователя.

          Мне вот интересен вариант, при котором .NET приложение, скомпилированное сразу в нативный код ngen'ом, будет иметь преимущество. Может кто подскажет? Или же это преимущество будет видно только, скажем, при загрузке приложения, когда дергается много разных методов?
          • –1
            > Ну тем не менее большинство десктопных донетных приложений прекрасно устраивают пользователя.

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

            > Или же это преимущество будет видно только, скажем, при загрузке приложения, когда дергается много разных методов?

            Ага, вы наверно рах в год загружаете приложения? Нормальный пользователь делает это постоянно, и ожидание раздражает.
            • 0
              установка в папку windows и ад dll — как раз то, от чего пытались избавиться создавая .net. Все зависит от желания разработчика, какую сборку он хочет создать. Фишка .net в том и заключается, что приложение полностью ложиться в папку и может быть удалено вместе с папкой без проблем. словно документ вордовский.
  • 0
    >тип, по-видимому — это аналог класса C#

    Тип это не аналог класса. Это категория более высокого порядка, чем класс. Он может быть классом, а может быть структурой (значимым типом), делегатом, перечислением или интерфейсом. Класс это одна из разновидностей типов объекта в .NET.
    • –1
      согласен, но для понимания проще думать что это класс, в рамках этой статьи.
  • 0
    Я не претендую на абсолютную правильность излагаемого материала. Это всего лишь мое понимание процесса

    Это новое веяние моды? Звучит престраннейше, еще и выделено. Если не секрет, зачем нужна техническая статья, в правильности которой не уверен даже автор?!? Т.е. читатель либо должен сам обращаться к первоисточникам, чтобы убедиться, что автор не ошибся, либо уже быть знакомым с материалом и «проверить» понимание автора?
    Если отойти от «предостережения», то соглашусь с bobermaniac
    Хорошая, годная копипаста.
    • 0
      чтобы в чем-то убедиться нужно взять и попробовать самому.
      • 0
        Дельный совет, только кому вы это рекомендуете?
        Если мне, то я большую часть из это пробовал еще лет семь назад. И сейчас все новое пробую.
        Если автору, то, наверное, ему следовало пробовать до публикации, чтобы не было необходимости писать в начале технической статьи
        Я не претендую на абсолютную правильность излагаемого материала
        • +1
          тогда я соглашусь с тем, кто тут писал про ярую нелюбовь к техническим статьям.
          Если вы пробовали это лет 7 назад, то почему вы уверены, что это вам должно понравиться и быть интересно и вообще написано для таких как вы? Это написанор для тех, кто знакомиться с .net и пытается разобраться что почем
          • 0
            А где я написал, что статья мне не понравилась?!
            Претензии я высказал к подходу. Вы не уверены в правильности написанного материала. Новички же, которые читают подобные статьи автоматически предполагают обратное, т.е. правильность статьи. Возможно, Вам стоило потратить еще немного времени, доразобраться, перепроверить, попробовать самому и не вставляться в начало статьи фразу о неуверенности выделенную болдом?
            • +1
              вы понимаете разницу между академическим текстом и описанием «своими словами»?
              В данном случае это — описание «своими словами». Естественно, что я считаю все приведенное здесь верным, но всегда оставляю вероятность что-то изложить не совсем точно. Хоть и весьма ничтожную вероятность…
  • 0
    это так странно, видеть CLR via C# на хабрахабре…

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