10 April 2010

Особенности работы 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#»
Tags:.netпрограммированиекомпиляторыC++clrframeworkmicrosoftC
Hubs: .NET
+7
56.3k 77
Comments 54