Skip to content
/ cecil Public
forked from jbevain/cecil

Commit

Permalink
Preserve custom debug information on types (#185)
Browse files Browse the repository at this point in the history
Portable PDBs may have CustomDebugInformation on many metadata
entities (see HasCustomDebugInformation in
https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#customdebuginformation-table-0x37).

For a select few of the CustomDebugInformation kinds (for example
state machine hoisted local scopes), cecil has dedicated types to
represent these in the object model. For the rest, cecil just reads
the custom debug info out of the blob heap as a `byte[]`.

When writing back a module, cecil walks the metadata as represented in
its object model, building the debug information as it goes.

So to support custom debug information for a new metadata token type:

- the corresponding cecil type should be made to implement
  `ICustomDebugInformationProvider`,

- the backing data for these getters should be populated on-demand,
  and also by the top-down walk in the immediate module reader, and

- the top-down walk of metadata for writing should visit the cecil
  object and write its debug information.

This change implements the above for `TypeDefinition`. It extends the
`ISymbolReader`/`ISymbolWriter` interfaces with a new method for
reading/writing custom debug info for any
`ICustomDebugInformationProvider`, and provides helpers for calling
the symbol reader that can be used when adding custom debug
information to other types in the future.

It doesn't include support for decoding the `TypeDefinitionDocument`
debug info - it continues representing these as
`BinaryCustomDebugInformation`.

---------

Co-authored-by: Jackson Schuster <[email protected]>
Co-authored-by: Andy Gocke <[email protected]>
  • Loading branch information
3 people authored Aug 13, 2024
1 parent a5915a2 commit ed276e7
Show file tree
Hide file tree
Showing 15 changed files with 200 additions and 51 deletions.
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\cecil.snk</AssemblyOriginatorKeyFile>
<DefineConstants>$(DefineConstants);NET_CORE</DefineConstants>
<RootNamespace></RootNamespace>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net40' ">
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
Expand Down
21 changes: 21 additions & 0 deletions Mono.Cecil.Cil/PortablePdb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using Mono.Collections.Generic;
using Mono.Cecil.Metadata;
using Mono.Cecil.PE;

Expand Down Expand Up @@ -145,6 +146,11 @@ void ReadStateMachineKickOffMethod (MethodDebugInformation method_info)
method_info.kickoff_method = debug_reader.ReadStateMachineKickoffMethod (method_info.method);
}

public Collection<CustomDebugInformation> Read (ICustomDebugInformationProvider provider)
{
return debug_reader.GetCustomDebugInformation (provider);
}

void ReadCustomDebugInformations (MethodDebugInformation info)
{
info.method.custom_infos = debug_reader.GetCustomDebugInformation (info.method);
Expand Down Expand Up @@ -221,6 +227,11 @@ public MethodDebugInformation Read (MethodDefinition method)
return reader.Read (method);
}

public Collection<CustomDebugInformation> Read (ICustomDebugInformationProvider provider)
{
return reader.Read (provider);
}

public void Dispose ()
{
reader.Dispose ();
Expand Down Expand Up @@ -319,6 +330,11 @@ public void Write ()
}
}

public void Write (ICustomDebugInformationProvider provider)
{
pdb_metadata.AddCustomDebugInformations (provider);
}

public ImageDebugHeader GetDebugHeader ()
{
if (IsEmbedded)
Expand Down Expand Up @@ -519,6 +535,11 @@ public void Write (MethodDebugInformation info)
writer.Write (info);
}

public void Write (ICustomDebugInformationProvider provider)
{
writer.Write (provider);
}

public ImageDebugHeader GetDebugHeader ()
{
ImageDebugHeader pdbDebugHeader = writer.GetDebugHeader ();
Expand Down
37 changes: 37 additions & 0 deletions Mono.Cecil.Cil/Symbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ public interface ISymbolReader : IDisposable {
ISymbolWriterProvider GetWriterProvider ();
bool ProcessDebugHeader (ImageDebugHeader header);
MethodDebugInformation Read (MethodDefinition method);
Collection<CustomDebugInformation> Read (ICustomDebugInformationProvider provider);
}

public interface ISymbolReaderProvider {
Expand Down Expand Up @@ -1116,6 +1117,7 @@ public interface ISymbolWriter : IDisposable {
ImageDebugHeader GetDebugHeader ();
void Write (MethodDebugInformation info);
void Write ();
void Write (ICustomDebugInformationProvider provider);
}

public interface ISymbolWriterProvider {
Expand Down Expand Up @@ -1224,5 +1226,40 @@ public static bool IsPortablePdb (Stream stream)
stream.Position = position;
}
}

public static bool GetHasCustomDebugInformations (
this ICustomDebugInformationProvider self,
ref Collection<CustomDebugInformation> collection,
ModuleDefinition module)
{
if (module.HasImage ()) {
module.Read (ref collection, self, static (provider, reader) => {
var symbol_reader = reader.module.symbol_reader;
if (symbol_reader != null)
return symbol_reader.Read (provider);
return null;
});
}

return !collection.IsNullOrEmpty ();
}

public static Collection<CustomDebugInformation> GetCustomDebugInformations (
this ICustomDebugInformationProvider self,
ref Collection<CustomDebugInformation> collection,
ModuleDefinition module)
{
if (module.HasImage ()) {
module.Read (ref collection, self, static (provider, reader) => {
var symbol_reader = reader.module.symbol_reader;
if (symbol_reader != null)
return symbol_reader.Read (provider);
return null;
});
}

Interlocked.CompareExchange (ref collection, new Collection<CustomDebugInformation> (), null);
return collection;
}
}
}
12 changes: 12 additions & 0 deletions Mono.Cecil/AssemblyReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ void ReadTypesSymbols (Collection<TypeDefinition> types, ISymbolReader symbol_re
{
for (int i = 0; i < types.Count; i++) {
var type = types [i];
type.custom_infos = symbol_reader.Read (type);

if (type.HasNestedTypes)
ReadTypesSymbols (type.NestedTypes, symbol_reader);
Expand Down Expand Up @@ -3160,6 +3161,17 @@ void InitializeCustomDebugInformations ()
}
}

public bool HasCustomDebugInformation (ICustomDebugInformationProvider provider)
{
InitializeCustomDebugInformations ();

Row<Guid, uint, uint> [] rows;
if (!metadata.CustomDebugInformations.TryGetValue (provider.MetadataToken, out rows))
return false;

return rows.Length > 0;
}

public Collection<CustomDebugInformation> GetCustomDebugInformation (ICustomDebugInformationProvider provider)
{
InitializeCustomDebugInformations ();
Expand Down
3 changes: 3 additions & 0 deletions Mono.Cecil/AssemblyWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,9 @@ void AddType (TypeDefinition type)
if (type.HasNestedTypes)
AddNestedTypes (type);

if (symbol_writer != null && type.HasCustomDebugInformations)
symbol_writer.Write (type);

WindowsRuntimeProjections.ApplyProjection (type, treatment);
}

Expand Down
18 changes: 17 additions & 1 deletion Mono.Cecil/TypeDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

using System;
using System.Threading;
using Mono.Cecil.Cil;
using Mono.Cecil.Metadata;
using Mono.Collections.Generic;

namespace Mono.Cecil {

public sealed class TypeDefinition : TypeReference, IMemberDefinition, ISecurityDeclarationProvider {
public sealed class TypeDefinition : TypeReference, IMemberDefinition, ISecurityDeclarationProvider, ICustomDebugInformationProvider {

uint attributes;
TypeReference base_type;
Expand All @@ -34,6 +35,8 @@ public sealed class TypeDefinition : TypeReference, IMemberDefinition, ISecurity
Collection<CustomAttribute> custom_attributes;
Collection<SecurityDeclaration> security_declarations;

internal Collection<CustomDebugInformation> custom_infos;

public TypeAttributes Attributes {
get { return (TypeAttributes) attributes; }
set {
Expand Down Expand Up @@ -284,6 +287,19 @@ public override Collection<GenericParameter> GenericParameters {
get { return generic_parameters ?? (this.GetGenericParameters (ref generic_parameters, Module)); }
}

public bool HasCustomDebugInformations {
get {
if (custom_infos != null)
return custom_infos.Count > 0;

return this.GetHasCustomDebugInformations (ref custom_infos, Module);
}
}

public Collection<CustomDebugInformation> CustomDebugInformations {
get { return custom_infos ?? (this.GetCustomDebugInformations (ref custom_infos, module)); }
}

#region TypeAttributes

public bool IsNotPublic {
Expand Down
49 changes: 49 additions & 0 deletions Test/Mono.Cecil.Tests/BaseTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Runtime.CompilerServices;
using Mono.Cecil.Cil;
using Mono.Cecil.Pdb;
using NUnit.Framework;

using Mono.Cecil.PE;
Expand Down Expand Up @@ -148,6 +149,54 @@ static void Run (TestCase testCase)
using (var runner = new TestRunner (testCase, TestCaseType.WriteFromImmediate))
runner.RunTest ();
}

public enum RoundtripType {
None,
Pdb,
PortablePdb
}

protected static ModuleDefinition RoundtripModule(ModuleDefinition module, RoundtripType roundtripType)
{
if (roundtripType == RoundtripType.None)
return module;

var file = Path.Combine (Path.GetTempPath (), "RoundtripModule.dll");
if (File.Exists (file))
File.Delete (file);

ISymbolWriterProvider symbolWriterProvider;
switch (roundtripType) {
case RoundtripType.Pdb when Platform.HasNativePdbSupport:
symbolWriterProvider = new PdbWriterProvider ();
break;
case RoundtripType.PortablePdb:
default:
symbolWriterProvider = new PortablePdbWriterProvider ();
break;
}

module.Write (file, new WriterParameters {
SymbolWriterProvider = symbolWriterProvider,
});
module.Dispose ();

ISymbolReaderProvider symbolReaderProvider;
switch (roundtripType) {
case RoundtripType.Pdb when Platform.HasNativePdbSupport:
symbolReaderProvider = new PdbReaderProvider ();
break;
case RoundtripType.PortablePdb:
default:
symbolReaderProvider = new PortablePdbReaderProvider ();
break;
}

return ModuleDefinition.ReadModule (file, new ReaderParameters {
SymbolReaderProvider = symbolReaderProvider,
InMemory = true
});
}
}

abstract class TestCase {
Expand Down
50 changes: 0 additions & 50 deletions Test/Mono.Cecil.Tests/ILProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
using System.IO;
using System.Linq;

using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Mdb;
using Mono.Cecil.Pdb;
using NUnit.Framework;

namespace Mono.Cecil.Tests {
Expand Down Expand Up @@ -499,12 +497,6 @@ static MethodBody CreateTestMethodWithLocalScopes (RoundtripType roundtripType,
return methodBody;
}

public enum RoundtripType {
None,
Pdb,
PortablePdb
}

static MethodBody RoundtripMethodBody(MethodBody methodBody, RoundtripType roundtripType, bool forceUnresolvedScopes = false, bool reverseScopeOrder = false)
{
var newModule = RoundtripModule (methodBody.Method.Module, roundtripType);
Expand Down Expand Up @@ -540,47 +532,5 @@ static void ReverseScopeOrder(ScopeDebugInformation scope)
foreach (var subScope in scope.Scopes)
ReverseScopeOrder (subScope);
}

static ModuleDefinition RoundtripModule(ModuleDefinition module, RoundtripType roundtripType)
{
if (roundtripType == RoundtripType.None)
return module;

var file = Path.Combine (Path.GetTempPath (), "TestILProcessor.dll");
if (File.Exists (file))
File.Delete (file);

ISymbolWriterProvider symbolWriterProvider;
switch (roundtripType) {
case RoundtripType.Pdb when Platform.HasNativePdbSupport:
symbolWriterProvider = new PdbWriterProvider ();
break;
case RoundtripType.PortablePdb:
default:
symbolWriterProvider = new PortablePdbWriterProvider ();
break;
}

module.Write (file, new WriterParameters {
SymbolWriterProvider = symbolWriterProvider,
});
module.Dispose ();

ISymbolReaderProvider symbolReaderProvider;
switch (roundtripType) {
case RoundtripType.Pdb when Platform.HasNativePdbSupport:
symbolReaderProvider = new PdbReaderProvider ();
break;
case RoundtripType.PortablePdb:
default:
symbolReaderProvider = new PortablePdbReaderProvider ();
break;
}

return ModuleDefinition.ReadModule (file, new ReaderParameters {
SymbolReaderProvider = symbolReaderProvider,
InMemory = true
});
}
}
}
42 changes: 42 additions & 0 deletions Test/Mono.Cecil.Tests/PortablePdbTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,43 @@ public void PortablePdbLineInfo()
}, symbolReaderProvider: typeof (PortablePdbReaderProvider), symbolWriterProvider: typeof (PortablePdbWriterProvider));
}

[Test]
public void TypeDefinitionDebugInformation ()
{
TestModule ("TypeDefinitionDebugInformation.dll", module => {
var enum_type = module.GetType ("TypeDefinitionDebugInformation.Enum");
Assert.IsTrue (enum_type.HasCustomDebugInformations);
var binary_custom_debug_info = enum_type.CustomDebugInformations.OfType<BinaryCustomDebugInformation> ().FirstOrDefault ();
Assert.IsNotNull (binary_custom_debug_info);
Assert.AreEqual (new Guid ("932E74BC-DBA9-4478-8D46-0F32A7BAB3D3"), binary_custom_debug_info.Identifier);
Assert.AreEqual (new byte [] { 0x1 }, binary_custom_debug_info.Data);

var interface_type = module.GetType ("TypeDefinitionDebugInformation.Interface");
Assert.IsTrue (interface_type.HasCustomDebugInformations);
binary_custom_debug_info = interface_type.CustomDebugInformations.OfType<BinaryCustomDebugInformation> ().FirstOrDefault ();
Assert.IsNotNull (binary_custom_debug_info);
Assert.AreEqual (new Guid ("932E74BC-DBA9-4478-8D46-0F32A7BAB3D3"), binary_custom_debug_info.Identifier);
Assert.AreEqual (new byte [] { 0x1 }, binary_custom_debug_info.Data);
}, symbolReaderProvider: typeof (PortablePdbReaderProvider), symbolWriterProvider: typeof (PortablePdbWriterProvider));
}

[Test]
public void ModifyTypeDefinitionDebugInformation ()
{
using (var module = GetResourceModule ("TypeDefinitionDebugInformation.dll", new ReaderParameters { SymbolReaderProvider = new PortablePdbReaderProvider () })) {
var enum_type = module.GetType ("TypeDefinitionDebugInformation.Enum");
var binary_custom_debug_info = enum_type.CustomDebugInformations.OfType<BinaryCustomDebugInformation> ().FirstOrDefault ();
Assert.AreEqual (new byte [] { 0x1 }, binary_custom_debug_info.Data);
binary_custom_debug_info.Data = new byte [] { 0x2 };

var outputModule = RoundtripModule (module, RoundtripType.None);
enum_type = outputModule.GetType ("TypeDefinitionDebugInformation.Enum");
binary_custom_debug_info = enum_type.CustomDebugInformations.OfType<BinaryCustomDebugInformation> ().FirstOrDefault ();
Assert.IsNotNull (binary_custom_debug_info);
Assert.AreEqual (new byte [] { 0x2 }, binary_custom_debug_info.Data);
}
}

public sealed class SymbolWriterProvider : ISymbolWriterProvider {

readonly DefaultSymbolWriterProvider writer_provider = new DefaultSymbolWriterProvider ();
Expand Down Expand Up @@ -730,6 +767,11 @@ public void Write ()
symbol_writer.Write ();
}

public void Write (ICustomDebugInformationProvider provider)
{
symbol_writer.Write (provider);
}

public void Dispose ()
{
symbol_writer.Dispose ();
Expand Down
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit ed276e7

Please sign in to comment.