Главная страница   /   7.3. Отладка внедренного кода (Метапрограммирование в .NET

Метапрограммирование в .NET

Метапрограммирование в .NET

Кевин Хазард

7.3. Отладка внедренного кода

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

Убираем путаницу с отладкой

Чтобы увидеть, насколько просто NotNullAttribute может «вогнать в ступор» разработчика, посмотрите на следующий код:

public class SomeClass
{
	public SomeClass([NotNull] string data)
	{
		this.Data = data;
	}
	public string Data { get; set; }
}

Давайте представим, что разработчик использует этот код вот так:

var data = new SomeClass(null);

Нигде в коде нет явной строки "выбросить новое ArgumentNullException". Конечно, есть атрибут [NotNull], следующий за аргументом данных, но если пользователи попытаются отладить этот код, все, что они получат, – это диалоговое окно, говорящее им, что было выброшено исключение. Но откуда? Откуда оно взялось?

Было бы лучше поменять файл отладки программы (.pdb) и изменить сборку, так чтобы разработчик мог увидеть, что NotNullAttribute что-то сделал. Например, наличие выделенного текста "NotNull" в отладчике, когда выполняются пять кодов операций, было бы хорошим визуальным индикатором во время отладки.

К счастью, Cecil поддерживает чтение информации об отладке, позволяя вам также редактировать ее. Давайте посмотрим, как вы можете добавить поддержку отладки для NotNullAttribute.

Загрузка и сохранение информации об отладке

Первое, что вы должны найти, это PDB файл, который содержит всю информацию об отладочных символах. Имейте в виду, что у вас, возможно, они не всегда будут. Это совершенно законно, если компилятор не создает отладочную информацию во время компиляции, или, может быть, PDB не был отправлен со сборкой, которую вы пытаетесь изменить. Таким образом, техники, показанные в разделе 7.3, должны быть защищенными, потому что, возможно, у вас не будет доступной отладочной информации. Чтобы загрузить отладочные символы, необходимо изменить код, который загружает сборку:

var assembly = AssemblyDefinition.ReadAssembly(assemblyLocation.FullName,
	new ReaderParameters { ReadSymbols = true });

Включите объект ReaderParameters в ReadAssembly() с установленным на true свойством ReadSymbols. Обратите внимание, что если вы попытаетесь загрузить сборку таким способом, а Cecil не может найти PDB файл, вы получите FileNotFoundException, поэтому нужно включить блок try-catch, чтобы обработать это ожидаемое исключение.

Кроме того, необходимо сохранить файл с информацией для отладки, если вы хотите сохранить любые изменения, внесенные в процессе модификации сборки:

assembly.Write(assemblyLocation.FullName,
	new WriterParameters() { WriteSymbols = true });

Проблемы с добавлением отладочной информации

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

Одной из проблем является выполнение модификация кода, когда нет кода, с которого можно начать. PDB файлы содержат только информацию, относящуюся к коду, который выполняется. В частности, PDB файл не содержит полную информацию сборки, например, имена типов. Он знает только, что конкретный код операции относится к некоторому куску текста в файле кода. Поэтому если вы добавляете код в сборку, вы, возможно, будете не в состоянии добавить символы, потому что вы не будете знать, где кодовый файл.

Давайте рассмотрим конкретный пример. В проекте Injectors есть еще один атрибут, который называется ToStringAttribute; он добавляет метод ToString() в класс, который соответствует соглашению, показанному в главах 2 и 4. Вы добавляете его в класс вот так:

[ToString]
public class MyClass { }

Если ToString не был определен для MyClass, атрибут внедрит новый метод ToString в класс. Но учтите, что у MyClass никогда не было переопределенного ToString(). Поэтому если PDB был создан во время выполнения, не будет никакой информации о файле MyClass.cs, который вы можете найти. Вполне возможно, что вы можете расширить код, чтобы получить информацию о файле кода во время его исполнения, в частности, если вы запускаете его в VS после каждого запуска проекта, но, в целом, PDB файл не всегда имеет информацию о кодовом файле, которую вы ищете.

Другая проблема – это разбор кода. Как вы увидите в следующем разделе, необходимо найти точный раздел кода, где текст "NotNull" или "NotNullAttribute" показан для параметра, который вы обнаружили и у которого есть эти метаданные, связанные с ним. На первый взгляд, это может показаться не таким сложным, но всегда есть проблемные места. Обычно когда вы добавляете атрибут к свойству в C#, это будет выглядеть вот так:

public void AMethod([NotNull] string value)

И следующий отрывок кода также является абсолютно валидным C#:

public void AMethod([NotNull]
	string value)

Удивительно, но это тоже верно:

public void AMethod([
	NotNull
	] string value)

Кроме того, помните, что несколько свойств могут иметь NotNullAttribute, и параметры также могут иметь другие пользовательские атрибуты, определенные для них:

public void AMethod([NotNull, MoreInformation] string value,
	[NotNull] string data)

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

Есть инструмент OSS IDE в .NET сообществе, называемый SharpDevelop (www.sharpdevelop.com). Поскольку он является IDE для разработки .NET приложений, он должен быть в состоянии разбирать код в IDE, чтобы предоставлять возможности, как IntelliSense. К счастью, этот парсинговый код поставляется в виде отдельной сборки с SharpDevelop, называемой ICSharpCode.NRefactory.dll. В следующем разделе вы увидите, как вы можете использовать эту сборку, чтобы сделать нахождение кода, который вы ищете в данном файле, безболезненным.

Совет

Вы можете получить NRefactory из Nuget: www.nuget.org/packages/ICSharpCode.NRefactory.

Добавление отладочной информации для внедренного кода

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

Определение парсингового класса

Во-первых, выясните, можете ли вы добавить отладочную информацию. Давайте немного изменим IL код в OnInject() для NotNullAttribute:

var processor = method.Body.GetILProcessor();
var first = processor.Body.Instructions[0];
var ldArgInstruction = processor.Create(OpCodes.Ldarg, target);
ldArgInstruction.SequencePoint =
	new NotNullAttributeParser(method, target).SequencePoint;
processor.InsertBefore(first, ldArgInstruction);

Вы должны обработать первую инструкцию иначе, чем другие четыре. Вы устанавливаете ее свойство SequencePoint для объекта SequencePoint, который содержит информацию об отладке, например, начальную и конечную строки, которые отладчику следует выделить, если встречается данный код операции.

NotNullAttributeDebugger – это класс, который содержит код, необходимый для добавления отладочной информации. Следующий листинг показывает, как конструктор устанавливает свое свойство SequencePoint.

Листинг 7-6: Создание парсера NRefactory
internal sealed class NotNullAttributeDebugger
{
	internal NotNullAttributeDebugger(
		MethodDefinition method, ParameterDefinition target)
	{
		this.SetPoint(method, target);
	}
	private void SetPoint(
		MethodDefinition method, ParameterDefinition target)
	{
		var point = method.FindSequencePoint();
		if(point != null)
		{
			using(var parser = ParserFactory.CreateParser(
				point.Document.Url))
			{
				parser.Parse();
				if(parser.Errors.Count <= 0)
				{
					var visitor = new NotNullAttributeVisitor(
						point.Document, method, target);
					parser.CompilationUnit.AcceptVisitor(
						visitor, null);
					this.SequencePoint = visitor.SequencePoint;
				}
			}
		}
	}
internal SequencePoint SequencePoint { get; private set; }

Метод расширенияFindSequencePoint() вернет первый объект SequencePoint, который он найдет в коллекции MethodDefinition Instruction. Вам нужен SequencePoint, потому что он содержит информацию о местоположении кодового файла, связанного с этим методом. Если вы обнаружите объект SequencePoint, тогда создается объект на основе IParser через метод CreateParser() из класса NRefactory ParserFactory. Кодовый файл содержится в свойстве Uri свойства SequencePoint Document. Если вызов Parse() не нашел никаких ошибок, будет создан объект NotNullAttributeVisitor, который передается в парсер. Это вложенный класс, определенный в NotNullAttributeDebugger, именно поэтому в этом фрагменте кода нет закрывающей фигурной скобки. (Вы увидите, как работают посетительские объекты, в следующем разделе). Метод SetPoint() установит свойство SequencePoint для объекта NotNullAttributeDebugger с новым объектом SequencePoint или null ссылкой. В следующем разделе вы увидите, как находится правильная часть "не-null" кода.

Нахождение правильного атрибута

В последнем разделе вы видели, что посетительский объект был передан AcceptVisitor(). NotNullAttributeVisitor наследуется от абстрактного класса AbstractAstVisitor. Этот класс содержит много методов "VisitXYZ", которые можно переопределить, чтобы найти конкретные части в кодовом файле, например, определение типа или метода. В данном случае вам нужно найти метод с тем же именем, что и метод, на который вы в настоящее время смотрите во фреймворке Injectors, а затем найти параметр с тем же именем, который имеет NotNullAttribute, определенной для него. С NRefactory это сделать просто. Давайте посмотрим на определение этого класса и два метода, которые следует переопределить, чтобы искать методы. Следующий код дает определение этого пользовательского посетителя.

Листинг 7-7: Создание посетителя NRefactory
using NR = ICSharpCode.NRefactory.Ast;
// ...
private sealed class NotNullAttributeVisitor : AbstractAstVisitor
{
	internal NotNullAttributeVisitor(
		Document document, MethodDefinition method,
		ParameterDefinition target)
	{
		this.Document = document;
		this.Method = method;
		this.Parameter = target;
	}
	public override object VisitConstructorDeclaration(
		NR.ConstructorDeclaration constructorDeclaration,
		object data)
	{
		this.VisitParametrizedNode(constructorDeclaration, true);
		return base.VisitConstructorDeclaration(
			constructorDeclaration, data);
	}
	public override object VisitMethodDeclaration(
		NR.MethodDeclaration methodDeclaration, object data)
	{
		this.VisitParametrizedNode(methodDeclaration, false);
		return base.VisitMethodDeclaration(
			methodDeclaration, data);
	}
	private Document Document { get; set; }
	private MethodDefinition Method { get; set; }
	private ParameterDefinition Parameter { get; set; }
	internal SequencePoint SequencePoint { get; private set; }

Один неудачный аспект использования NRefactory и Cecil вместе заключается в том, что они используют одни и те же имена типов в ряде случаев, например, MethodDeclaration.

Поэтому добавьте выражение using, чтобы определить, используете вы класс, определенный либо в NRefactory, либо Cecil сборках. Как вы можете видеть, вам нужно только переопределить два метода, VisitConstructorDeclaration() и VisitMethodDeclaration(). Поскольку ConstructorDeclaration и MethodDeclaration оба наследуются от базового класса ParametrizedNode, который определяет необходимые вам члены, этот парсинговый код определен в общем методе VisitParametrizedNode(). Следующий код показывает, как VisitParametrizedNode() создает SequencePoint, который вам нужен.

Листинг 7-8: Создание SequencePoint для NotNullAttribute
private void VisitParametrizedNode(
	NR.ParametrizedNode node, bool isConstructor)
{
	if(((isConstructor && this.Method.IsConstructor) ||
		(node.Name == this.Method.Name)) &&
		node.Parameters.Count == this.Method.Parameters.Count)
	{
		var doParametersMatch = true;
		NR.ParameterDeclarationExpression matchingParameter = null;
		for(var i = 0; i < node.Parameters.Count; i++)
		{
			var parsedParameter = node.Parameters[i];
			if(parsedParameter.ParameterName !=
				this.Method.Parameters[i].Name)
			{
				doParametersMatch = false;
				break;
			}
			else if(parsedParameter.ParameterName ==
				this.Parameter.Name)
			{
				matchingParameter = parsedParameter;
			}
		}
		if(doParametersMatch && matchingParameter != null)
		{
			this.SequencePoint =
				(from attributeSection in matchingParameter.Attributes
				from attribute in attributeSection.Attributes
				where (attribute.Name == "NotNullAttribute" ||
					attribute.Name == "NotNull")
				select new SequencePoint(this.Document)
				{
					EndColumn = attribute.EndLocation.Column,
					EndLine = attribute.EndLocation.Line,
					StartColumn = attribute.StartLocation.Column,
					StartLine = attribute.StartLocation.Line
				}).Single();
		}
	}
}

Строки 4-6: Определить по имени, является ли это корректным методом

Строки 13-24: Верифицировать параметры

Строки 28-39: Найти правильный NotNullAttribute внутри параметров

Проверьте, является ли текущий метод в кодовом файле AST тем, что нашла Cecil. Вы должны сделать эту верификацию по имени, но если метод является конструктором, это имя не будет соответствовать тому, что Cecil будет называть конструктором: .ctor. Вот почему проверка имени кажется немного запутанной.

После верификации имени метода, следующим шагом является проверка имен параметров. Опять же, все что у вас есть – это кодовый файл; на данный момент у вас нет информации о типах. Лучшая проверка заключается в том, чтобы увидеть, является ли число параметров таким же и располагаются ли имена в том же порядке, как Cecil находит в сборке. Если это так, нахождение атрибута является только LINQ выражением. Вы перемещаете информацию по атрибуту в соответствующий параметр, чтобы найти местонахождение текста "NotNull" или "NotNullAttribute". Вы можете использовать свойства этого атрибута StartLocation и EndLocation для создания SequencePoint, отмеченного правильным местоположением текста.

Наблюдение за результатами

Теперь, когда вы добавили поддержку отладки для NotNullAttribute, что произойдет, если вы запустите код, который использует атрибут, в отладчике? Рисунок 7-3 показывает результат. Этот скриншот был сделан, когда мы вошли в метод. Отладчик правильно выделяет текст "NotNull", и теперь разработчик имеет гораздо более хороший индикатор, что в метод был добавлен код, что связано с наличием атрибута.

Рисунок 7-3: Выделение атрибутов. Вы можете четко видеть, что код связан с NotNullAttribute.

Теперь вы знаете, как «вплести» код в сборку и обеспечить поддержку отладки. Это определенно не просто, но, к счастью, в связи с тяжелой работой людей, которые создали и поддерживают Cecil и NRefactory, это относительно безболезненно.