Generating unique and persisted numbers for C# properties

In previous steps I described how I've modified the Roslyn compiler to generate unique numbers for the following items:

  • C# projects (modules / assemblies)
  • .Net types (classes and structs) in the program
  • fields defined in the types
  • methods defined in the types

Now the turn has come to the properties defined in the C# classes and structs in the programs.

As usual, an important aspect of this is that if there are public properties that are decorated with a [YacksSerialization()] attribute that implies binary serialization then these unique property numbers should be persisted in a LiteDB database, so they remain the same for every compilation. This is necessary to ensure that other programs that reference these public properties via their property numbers (via binary serialized data) will still work unchanged after rebuilding selected modules.

(The current implementation, as seen below, is wrong - it always gets a persisted property number for all public properties if the YacksAnonymizeModule option is on, but this is not necessary - as far as I can tell there is no cross-module referencing of property names at runtime.)

Note: The code shown below is somewhat obsolete. A newer version is available for download - see this article.

src\Compilers\Core\Portable\YacksCompilation.cs

See previous articles for previous (and now obsolete) listings of this source file. Here are some of the changes made in this iteration.

Around line 53:

// Length of numeric parts of module names such as "Merlinia1585" and type names such as // "T535$0045" or "t535$0045" and field names such as "F0003" or "f0003" and method names // such as "M0007" or "m0007" and property names such as "P0077" or "p0077". These must be // kept coordinated with the following max values. internal const int CModuleNumberLength = 4; internal const int CTypeNumberLength = 4; internal const int CFieldNumberLength = 4; internal const int CMethodNumberLength = 4; internal const int CPropertyNumberLength = 4; // Max values for the numeric parts of type names such as "T535$0045" or "t535$0045" and field // names such as "F0003" or "f0003" and method names such as "M0007" or "m0007" and property // names such as "P0077" or "p0077". These must be kept coordinated with the above values for // number of digits. private const int CTypeNumberMax = 9999; private const int CFieldNumberMax = 9999; private const int CMethodNumberMax = 9999; private const int CPropertyNumberMax = 9999;

Around line 113:

// The non-persisted type numbers and field numbers and property numbers for this project // start at 0001 and work their way up, limit being 9999 private int _nonPersistedTypeNumber = 0; private int _nonPersistedFieldNumber = 0; private int _nonPersistedPropertyNumber = 0;

Around line 466:

/// <summary> /// Method to get a persisted property number for a property defined in a type in the current /// project. /// /// Unlike for fields, this is currently implemented without using any YacksPropertyInfo /// object. /// </summary> /// <param name="typeInfo">YacksTypeInfo object for the containing type</param> /// <param name="propertyName">name of the property</param> /// <returns>property number, 1 - 9999, or -1 if something wrong</returns> internal int GetOrCreatePropertyNumber(YacksTypeInfo typeInfo, string propertyName) { // Check the property number dictionary exists, create it if not if (typeInfo.PropertyDictionary == null) typeInfo.PropertyDictionary = new Dictionary<string, int>(); // Test for the specified property already defined in a previous compilation, return its // number if so int propertyNumber = typeInfo.GetPropertyNumber(propertyName); if (propertyNumber != -1) return propertyNumber; // Update the LastPropertyNumber field in the YacksTypeInfo object for the containing type, // add it to the dictionary of known property names, write the Yacks metadata for this // project to the disk, return property number propertyNumber = ++typeInfo.LastPropertyNumber; typeInfo.AddPropertyNumber(propertyName, propertyNumber); _yacksProjects.Update(_projectInfo); return propertyNumber; } /// <summary> /// Method to get a property number that should not be persisted, i.e., for a non-public /// property, including a property in a non-public type. /// </summary> /// <returns>property number, 1 - 9999, or -1 if something wrong</returns> internal int GetNonPersistedPropertyNumber() { // Increment the non-persisted property number and check it hasn't reached max value, which // should be totally impossible, would require a project with 10000 non-public properties _nonPersistedPropertyNumber += 1; return _nonPersistedPropertyNumber <= CPropertyNumberMax ? _nonPersistedPropertyNumber : -1; }

src\Compilers\Core\Portable\YacksMetadata-TypeInfo.cs

This file has been listed in several previous steps. In this article I'll just list some of the changes since the last step.

// Last (persisted) property number that has been assigned to a property in this type, i.e., // next property number should be this number plus one. This is only relevant for persisted // property numbers, which is normally only needed for public properties if a // [YacksSerialization()] attribute that specifies "B" (binary serialization) has been used. // This is somewhat redundant since the number of elements in PropertyDictionary should give // the same result, at least for the current implementation. public int LastPropertyNumber { get; set; } = 0; // This dictionary records the property numbers generated for the public properties in this // class or struct. The key is the property name and the value is the property number. public Dictionary<string, int> PropertyDictionary { get; set; } = null; /// <summary> /// Method to get a persisted property number for a property defined in this class or struct. /// </summary> /// <param name="propertyName">name of the property</param> /// <returns>property number, 1 - 9999, or -1 if not defined</returns> public int GetPropertyNumber(string propertyName) { int propertyNumber; if (PropertyDictionary.TryGetValue(propertyName, out propertyNumber)) return propertyNumber; return -1; } /// <summary> /// Method to add a persisted property number for a property defined in this class or struct to /// the dictionary that persists this information. /// </summary> /// <param name="propertyName">name of the property</param> /// <param name="propertyNumber">property number, 1 - 9999</param> public void AddPropertyNumber(string propertyName, int propertyNumber) { PropertyDictionary.Add(propertyName, propertyNumber); }

src\Compilers\Core\Portable\PEWriter\MetadataWriter.cs

This source file is part of the CodeAnalysis project in Roslyn. In the Visual Studio Solution Explorer it can be found under CodeAnalysis - PEWriter.

Compared with the previous steps one more method has been modified.

private void PopulatePropertyTableRows() { var propertyDefs = this.GetPropertyDefs(); metadata.SetCapacity(TableIndex.Property, propertyDefs.Count); foreach (IPropertyDefinition propertyDef in propertyDefs) { //Yacks12: Anonymize the property name if necessary PropertyAttributes propertyAttributes = GetPropertyAttributes(propertyDef); StringHandle methodNameHandle = AnonymizePropertyName(propertyDef, propertyAttributes); metadata.AddProperty( //attributes: GetPropertyAttributes(propertyDef), attributes: propertyAttributes, //name: GetStringHandleForNameAndCheckLength(propertyDef.Name, propertyDef), name: methodNameHandle, signature: GetPropertySignatureHandle(propertyDef)); } }

src\Compilers\Core\Portable\PEWriter\MetadataWriter.Yacks.cs

This is a source file which has been added to the CodeAnalysis project. One method previously shown has been modified and two additional methods have been added.

/// <summary> /// Method to either "anonymize" a property name in a C# class or struct if applicable and /// possible, or else to do standard processing to emit the property definition. (Some of the /// code in this method is copied from original code in the /// MetadataWriter.PopulatePropertyTableRows() method.) /// </summary> private StringHandle AnonymizePropertyName(IPropertyDefinition propertyDefinition, PropertyAttributes propertyAttributes) { // Test if applicable to "anonymize" the method name (INamedTypeDefinition containingTypeDefinition, string namespaceName) = GetNamespaceName(propertyDefinition); if (namespaceName != null && EmittingAnonymized(namespaceName)) { // Get the YacksTypeInfo object for the containing type. If this is not available it // indicates the property is defined in a non-public type. (YacksCompilation yacksCompilation, YacksTypeInfo typeInfo) = GetContainingTypeInfo(containingTypeDefinition, namespaceName); // Get the persisted Yacks metadata property number for this property if possible, or // for non-public properties get a non-persisted property number. // NB. This is wrong - it should only get a persisted property number if a // a [YacksSerialization()] attribute has been used that implies binary serialization. ISymbol propertySymbol = propertyDefinition as ISymbol; Accessibility? declaredAccessibility = propertySymbol?.DeclaredAccessibility; bool isPublic = typeInfo != null && declaredAccessibility != null && declaredAccessibility == Accessibility.Public; int fieldNumber = isPublic ? yacksCompilation.GetOrCreatePropertyNumber(typeInfo, propertyDefinition.Name) : yacksCompilation.GetNonPersistedPropertyNumber(); if (fieldNumber != -1) { // The property name gets "anonymized", becoming "Pnnnn" or "pnnnn" return GetHandleForAnonymizedPropertyName(fieldNumber, isPublic); } } // Property name does not get anonymized - do standard processing return GetStringHandleForNameAndCheckLength(propertyDefinition.Name, propertyDefinition); } /// <summary> /// Method to get the namespace name for a field or method or property definition. This also /// returns a reference to the containing type definition. /// </summary> private (INamedTypeDefinition containingTypeDefinition, string namespaceName) GetNamespaceName(ITypeDefinitionMember memberDefinition) { INamedTypeDefinition containingTypeDefinition = memberDefinition.ContainingTypeDefinition as INamedTypeDefinition; string namespaceName = containingTypeDefinition?.AsNamespaceTypeDefinition(Context)?.NamespaceName; return (containingTypeDefinition, namespaceName); } /// <summary> /// Method to format an anonymized property name and then to add it to the PE module #Strings /// stream and return the "handle" for the string. An anonymized property name is "Pnnnn" or /// "pnnnn", where nnnn is the persisted property number within the type or the non-persisted /// property number within the project, respectively. /// </summary> private StringHandle GetHandleForAnonymizedPropertyName(int propertyNumber, bool isPublic) { return metadata.GetOrAddString((isPublic ? "P" : "p") + propertyNumber.ToString( "D" + YacksCompilation.CPropertyNumberLength, CultureInfo.InvariantCulture)); }

Testing

As usual, I used a (slightly modified) library assembly Merlinia.CommonClasses.MArrays to test the modified Roslyn compiler. These screen shots (with highlighting added) show the PropertyMap and Property metadata for the non-anonymized and the anonymized versions of the MArrays assembly as displayed by JetBrains dotPeek.

ModRos 12 Snap1

ModRos 12 Snap2 

 Finally I'll just mention that calling this code from another program did not result in any problems.

You must login to post a comment.
Loading comment... The comment will be refreshed after 00:00.

Be the first to comment.