Как компиляция C # обходится без заголовочных файлов? - PullRequest
29 голосов
/ 17 декабря 2009

Я провел свою профессиональную жизнь в качестве разработчика на C #. Будучи студентом, я иногда использовал C, но не изучал глубоко его модель компиляции. Недавно я вскочил на подножку и начал изучать Objective-C. Мои первые шаги только заставили меня осознать дыры в моих ранее существующих знаниях.

Из моего исследования, компиляция C / C ++ / ObjC требует, чтобы все встречающиеся символы были предварительно объявлены. Я также понимаю, что строительство - это двухэтапный процесс. Сначала вы компилируете каждый отдельный исходный файл в отдельные объектные файлы. Эти объектные файлы могут иметь неопределенные «символы» (которые обычно соответствуют идентификаторам, объявленным в заголовочных файлах). Во-вторых, вы связываете объектные файлы вместе, чтобы сформировать окончательный результат. Это довольно высокоуровневое объяснение, но оно удовлетворяет мое любопытство. Но я также хотел бы иметь подобное понимание высокого уровня процесса сборки C #.

Q: Как процесс сборки C # позволяет обойтись без заголовочных файлов? Я мог бы предположить, что шаг компиляции выполняется в два прохода?

(Правка: ответьте на вопрос здесь Как C / C ++ / Objective-C сравниваются с C #, когда речь идет об использовании библиотек? )

Ответы [ 5 ]

91 голосов
/ 17 декабря 2009

ОБНОВЛЕНИЕ: Этот вопрос был темой моего блога за 4 февраля 2010 года . Спасибо за отличный вопрос!

Позвольте мне выложить это для вас. В самом простом смысле компилятор является «двухпроходным компилятором», потому что фазы, которые проходит компилятор:

1) Генерация метаданных . 2) Генерация IL .

Метаданные - это все материалы «верхнего уровня», которые описывают структуру кода. Пространства имен, классы, структуры, перечисления, интерфейсы, делегаты, методы, параметры типа, формальные параметры, конструкторы, события, атрибуты и так далее. По сути, все , кроме тел методов.

IL - это все, что входит в тело метода - фактический императивный код, а не метаданные о том, как код структурирован.

Первый этап фактически осуществляется через множество проходов через источники. Это больше, чем два.

Первое, что мы делаем, это берем текст источников и разбиваем его на поток токенов. То есть мы делаем лексический анализ, чтобы определить, что

class c : b { }

- это класс, идентификатор, двоеточие, идентификатор, левый вьющийся, правый вьющийся.

Затем мы выполняем «синтаксический анализ верхнего уровня», где мы проверяем, что потоки токенов определяют грамматически правильную программу на C #. Однако мы пропускаем тела метода разбора. Когда мы попадаем в тело метода, мы просто пролистываем токены, пока не доберемся до кудрявого близкого совпадения. Мы вернемся к этому позже; на данный момент мы заботимся только о том, чтобы получить достаточно информации для создания метаданных.

Затем мы делаем проход «объявление», где делаем пометки о расположении каждого пространства имен и объявления типа в программе.

Затем мы делаем проход, где проверяем, что все объявленные типы не имеют циклов в своих базовых типах. Сначала нам нужно сделать это, потому что при каждом последующем проходе мы должны иметь возможность обходить иерархии типов без необходимости иметь дело с циклами.

Затем мы делаем проход, где проверяем, что все ограничения универсальных параметров для универсальных типов также являются ациклическими.

Затем мы выполняем этап, где проверяем, является ли каждый член каждого типа - методы классов, поля структур, перечислимые значения и т. Д. - согласованными. Нет циклов в перечислениях, каждый переопределяющий метод переопределяет то, что на самом деле является виртуальным, и так далее. На этом этапе мы можем вычислить «vtable» макеты всех интерфейсов, классов с виртуальными методами и т. Д.

Затем мы делаем проход, где мы прорабатываем значения всех полей "const".

На данный момент у нас достаточно информации, чтобы выдать почти все метаданные для этой сборки. У нас до сих пор нет информации о метаданных для замыканий итераторов / анонимных функций или анонимных типов; мы делаем это поздно.

Теперь мы можем начать генерировать IL. Для каждого тела метода (и свойств, индексаторов, конструкторов и т. Д.) Мы перематываем лексер до точки начала тела метода и анализируем тело метода.

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

Сначала мы запускаем проход, чтобы преобразовать циклы в goto и метки.

(Следующие несколько проходов ищут плохие вещи.)

Затем мы запускаем проход для поиска использования устаревших типов, для предупреждений.

Затем мы запускаем проход, который ищет использование анонимных типов, для которых мы еще не выдавали метаданные, и выдает их.

Затем мы запускаем проход, который ищет неправильное использование деревьев выражений. Например, используя оператор ++ в дереве выражений.

Затем мы запускаем проход, который ищет все локальные переменные в теле, которые определены, но не используются, для сообщения о предупреждениях.

Затем мы запускаем проход, который ищет недопустимые шаблоны внутри блоков итераторов.

Затем мы запускаем проверку достижимости, чтобы выдавать предупреждения о недоступном коде, и сообщаем вам, когда вы сделали что-то вроде забытого возврата в конце не пустого метода.

Затем мы запускаем проход, который проверяет, что каждое goto нацелено на разумную метку, и что каждая метка нацелена на достижимое goto.

Затем мы запускаем проход, который проверяет, что все локальные объекты определенно назначены перед использованием, отмечает, какие локальные переменные являются закрытыми над внешними переменными анонимной функции или итератора и какие анонимные функции находятся в достижимом коде. (Этот проход делает слишком много. Я уже давно собираюсь провести его рефакторинг.)

На данный момент мы закончили искать плохие вещи, но у нас еще есть много проходов, прежде чем мы заснем.

Далее мы запускаем проход, который обнаруживает отсутствующие аргументы ref для вызовов COM-объектов и исправляет их. (Это новая функция в C # 4.)

Затем мы запускаем проход, который ищет материал в форме «new MyDelegate (Foo)» и переписывает его в вызов CreateDelegate.

Затем мы запускаем проход, который преобразует деревья выражений в последовательность вызовов фабричных методов, необходимых для создания деревьев выражений во время выполнения.

Затем мы запускаем проход, который переписывает всю обнуляемую арифметику в код, который проверяет HasValue и т. Д.

Затем мы запускаем проход, который находит все ссылки формы base.Blah () и переписывает их в код, который выполняет не виртуальный вызов метода базового класса.

Затем мы запускаем проход, который ищет инициализаторы объектов и коллекций и превращает их в соответствующие наборы свойств и т. Д.

Затем мы запускаем проход, который ищет динамические вызовы (в C # 4) и переписывает их в сайты динамических вызовов, которые используют DLR.

Затем мы запускаем проход, который ищет вызовы удаленных методов. (То есть частичные методы без фактической реализации или условные методы, для которых не определен символ условной компиляции.) Они превращаются в no-ops.

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

Затем мы запускаем этап оптимизации, который переписывает тривиальные операторы "is" и "as".

Затем мы запускаем этап оптимизации, который ищет переключатель (константу) и переписывает его как ответвление непосредственно в правильный регистр.

Затем мы запускаем проход, который превращает конкатенацию строк в вызовы правильной перегрузки String.Concat.

(Ах, воспоминания. Эти два последних прохода были первыми вещами, над которыми я работал, когда присоединился к команде компиляторов.)

Затем мы запускаем проход, который переписывает использование именованных и необязательных параметров в вызовы, где все побочные эффекты происходят в правильном порядке.

Затем мы запускаем проход, который оптимизирует арифметику; например, если мы знаем, что M () возвращает int, и у нас есть 1 * M (), то мы просто превращаем его в M ().

Затем мы генерируем код для анонимных типов, впервые использованных этим методом.

Затем мы преобразуем анонимные функции в этом теле в методы классов замыкания.

Наконец, мы преобразуем блоки итераторов в конечные автоматы на основе коммутаторов.

Затем мы испускаем IL для преобразованного дерева, которое мы только что вычислили.

Легко, как пирог!

37 голосов
/ 17 декабря 2009

Я вижу, что есть несколько толкований вопроса. Я ответил на интерпретацию внутри решения, но позвольте мне заполнить ее всей информацией, которую я знаю.

«Метаданные заголовочного файла» присутствуют в скомпилированных сборках, поэтому любая сборка, на которую вы добавляете ссылку, позволит компилятору извлекать метаданные из них.

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

Это позволяет компилятору знать, что существует, а что нет (в его вселенной).

Чтобы увидеть действующий двухпроходный компилятор, протестируйте следующий код, который имеет 3 проблемы, две проблемы, связанные с объявлением, и одну проблему кода:

using System;

namespace ConsoleApplication11
{
    class Program
    {
        public static Stringg ReturnsTheWrongType()
        {
            return null;
        }

        static void Main(string[] args)
        {
            CallSomeMethodThatDoesntExist();
        }

        public static Stringg AlsoReturnsTheWrongType()
        {
            return null;
        }
    }
}

Обратите внимание, что компилятор будет жаловаться только на два Stringg типа, которые он не может найти. Если вы их исправите, то получит жалобу на имя метода, вызванное в методе Main, которое не может найти.

5 голосов
/ 17 декабря 2009

Используются метаданные эталонных сборок. Он содержит полное объявление типа, то же самое, что вы найдете в заголовочном файле.

Это двухпроходный компилятор, выполняющий что-то еще: вы можете использовать тип в одном исходном файле, прежде чем он будет объявлен в другом файле исходного кода.

1 голос
/ 17 декабря 2009

Всю необходимую информацию можно получить из ссылочных сборок.

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

И да, это двухпроходный компилятор, но он не объясняет, как он получает информацию о типах библиотек.

1 голос
/ 17 декабря 2009

Это двухпроходный компилятор. http://en.wikipedia.org/wiki/Multi-pass_compiler

...