The issue
Last week, I faced a challenge with Delphi runtime type information attributes and Enum types. I needed to have an Enum for which each value had a string representation (something that Rust allows majestically). My first approach was to create a simple custom attribute like this:
MappingAttribute = class(TCustomAttribute)
private
FValue: String;
public
constructor Create(AValue: String);
property Value: string read FValue;
end;
Then, use it directly in the enum’s values as follows:
EnumCars = (
[MappingAttribute('VOLKSWAGEN')] ecVolkswagen,
[MappingAttribute('GENERAL MOTORS')] ecChevrolet,
[MappingAttribute('FORD MOTORS')] ecFord,
[MappingAttribute('RENAULT')] ecRenault
);
It turned out not to be a valid way of using attributes. According to this StackOverflow answer, Win32 RTTI does not store attributes that are associated with enumeration values as a trade-off to retain storage. The answer also explains why RAD Studio does not complain about the syntax if it will not be stored: it’s to keep backward compatibility with .NET, which seems to allow this behavior.
The solution
Since my first approach was impossible, I had to take another path. In my own code I also had to account to the fact that I had lots of Enum Types to deal with. That’s why I thought about using generics. My reformulated approach was to keep the MappingAttribute I have previously defined and set the attributes in the Enum Type instead of into its values, for example:
Since my first approach was impossible, I had to take another path. In my own code, I also had to account for the fact that I had many enum types to deal with. That’s why I thought about using generics. My reformulated approach was to keep the MappingAttribute I had previously defined and set the attributes in the enum type instead of in its values, for example:
[
MappingAttribute('VOLKSWAGEN'),
MappingAttribute('GENERAL MOTORS'),
MappingAttribute('FORD MOTORS'),
MappingAttribute('RENAULT')
]
EnumCars = (
ecVolkswagen,
ecChevrolet,
ecFord,
ecRenault
);
This way, I defined attributes for the type that will be stored by Win32 RTTI and linked to the values by their order. The order in which they are defined is the order in which they will be retrieved by the TRTTIObject.GetAttributes() function. I recognize this is a very loose coupling, and it also depends on some assumptions to work:
- For each enumerated value in a determined mapped enum type, there is one and only one attribute, and this attribute is of type
MappingAttribute. - The enum type does not define a custom ordinality. This has two explanations. The first is that attributes and members of the enum type are coupled by the automatically assigned ordinal value of each member. The second is that enum types with custom ordinality do not have runtime type information, so they wouldn’t store the attributes at all.
There could be more implicit assumptions, but these two are the ones I deduced.
Now that I had attributes that I could access, I had to make sure that it would be possible to extract the attribute for any enum type where I needed to use it, and also set an enum value based on its string representation.
In Delphi, we cannot declare generic functions or procedures, only generic methods (i.e., the generic function or procedure must be a member of a composite type). For this reason, I declared mine inside a class type.
TEnumMapping = class
public
class function EnumToString<T>(Value: T): String; static;
class function StringToEnum<T>(Value: String): T; static;
end;
The EnumToString function
The definition of the first function, TEnumMapping.EnumToString has the following header:
class function TEnumMapping.EnumToString<T>(Value: T): String;
var
Ctx: TRTTIContext;
RTTIType: TRTTIType;
RTTIEnumType: TRTTIEnumerationType;
GenericValue: TValue;
OrdValue: Int64;
begin
...
end;
I started by creating the TRTTIContext object, getting the type information, and checking basic preconditions.
//...
Ctx := TRTTIContext.Create();
try
RTTIType := Ctx.GetType(TypeInfo(T));
if RTTIType = nil then
raise Exception.Create('The type has no Runtime Type Information');
if RTTIType.TypeKind <> tkEnumeration then
raise Exception.CreateFmt('Type %s is not an enumeration', [RttiType.Name]);
finally
Ctx.Free();
end;
//...
The code following the creation of the runtime context is within a try...finally block to ensure that the reference to the TRTTIContext is released at the end. It’s worth noting that the TRTTIContext type is a singleton, thus it’s only created once. The call to Ctx.Free() only releases the reference to the instance. The two if...then blocks at the start of the try...finally block are examples of early returns: If the required preconditions are not met, an exception is raised and the function has its execution halted. This ensures that if the code continues its execution, the operations dependent on these preconditions are safe to execute.
The next logical step is to create a TValue to extract the ordinal value of the enumerated type T (T is guaranteed to be an enumerated type by the early returns):
//...
GenericValue := TValue.From<T>(Value);
if not GenericValue.TryAsOrdinal(OrdValue) then
raise Exception.Create('Could not cast generic value to ordinal');
//...
The creation of the TValue is guaranteed, so I only needed to check if it was possible to cast its value as an ordinal type, raise an exception if it fails, or proceed if it doesn’t. Then, I cast the generic instance of TRTTIType to TRTTIEnumerationType. This cast is safe because the preconditions were already checked and an exception was not raised. The cast is necessary to access the MaxValue and MinValue properties of the enum type. These properties were then used to evaluate whether the OrdValue is within the range of the enum type.
//...
RTTIEnumType := TRTTIEnumerationType(RTTIType);
if (OrdValue > RTTIEnumType.MaxValue) or (OrdValue < RTTIEnumType.MinValue) then
raise Exception.CreateFmt('%d has no valid enum name for %s', [OrdValue, RTTIType.Name]);
//...
Finally, I check if the number of attributes is the same as the number of elements, which is the last condition I needed to verify before being able to return a reliable value. If the check fails, the code raises an exception; otherwise, it returns the string value through the System.Exit function.
//...
var LenStore: Integer := Length(RTTIEnumType.GetAttributes);
if (LenStore = 0) or
(LenStore - 1 <> RTTIEnumType.MaxValue) then
raise Exception.CreateFmt('RTTI Mapping in %s is badly formated.', [RTTIEnumType.Name]);
exit(MappingAttribute(RTTIEnumType.GetAttributes()[OrdValue]).Value);
//...
This finishes the TEnumMapping.EnumToString<T>(Value: T) function.
The StringToEnum function
The definition of TEnumMapping.StringToEnum<T>(Value: T) starts almost the same as TEnumMapping.EnumToString<T>(Value: T), but in this one, I only declared two variables in the header: the ones to hold TRTTIContext and TRTTIType. For the rest, I leveraged the new (since Delphi 10.3) possibility of declaring Inline Variables.
The first few lines of the function are also identical to the previously cited one, beginning with the creation of the context and then entering a `try…finally` block. Then, it uses the context to get the type information and check if there’s RTTI and if the type is an enumeration.
class function TEnumMapping.StringToEnum<T>(Value: String): T;
var
Ctx: TRTTIContext;
RTTIType: TRTTIType;
begin
Ctx := TRTTIContext.Create();
try
RTTIType := Ctx.GetType(TypeInfo(T));
if RTTIType = nil then
raise Exception.Create('The type is has no runtime information');
if (RTTIType.TypeKind <> tkEnumeration) then
raise Exception.Create('The type is not an enumeration');
//...
finally
Ctx.Free();
end;
end;F
Following the previous block of code, I believe it is a good idea to provide a default value to the function’s Result variable:
//...
FillChar(Result, SizeOf(Result), 0);
//...
This ultimately assigns the value 0 to Result. Providing a default value ensures that Result has a valid state even if something goes unexpectedly wrong. After that, I declared two inline variables to store the number of attributes and the number of members in the enumeration.
//...
var ItemCount, AttrCount: Integer;
ItemCount := Length(TRTTIEnumerationType(RTTIType).GetNames());
AttrCount := Length(RTTIType.GetAttributes());
//...
The method to get the number of attributes was already presented in this post. However, the method to get the number of enumerated values within an enumeration is appearing for the first time. I reviewed Delphi’s RTTI library documentation and could not find another reliable way to get the number of members, so I obtained the count of unique names within the enumeration.
One might argue that it’s achievable by checking the MaxValue property of the TRTTIEnumerationType. Indeed, it is possible. If the enumeration has three members, once the execution reaches the ItemCount variable’s line, it is guaranteed that there is no custom ordinality (because if there were, there would be no runtime type information, and an exception would be raised). Therefore, MaxValue + 1 is equal to the number of members within the enumeration, 3, as it is zero-indexed. My choice to keep the GetNames() method is to keep it future-proof (in my opinion, of course). If, in the future, Win32 stores RTTI for custom ordinality enumerations, there will be fewer changes needed in the function.
The next step was to check if the number of elements and the number of attributes are the same.
//...
if (AttrCount <> ItemCount) then
raise Exception.CreateFmt('RTTI Mapping in %s is badly formated.', [RTTIType.Name]);
//...
Originally, I checked if AttrCount was equal to zero or different from ItemCount, but because Delphi disallows empty enumerations, the former makes no sense.
Following, the next step was to iterate over the attributes and find the one that matches. In my code, I made it case-sensitive, but it’s purely a design choice and can be changed or even improved by allowing an optional argument to dictate whether the search is case-sensitive or not.
The logic I used was to iterate over the attributes of type T and check the value of each attribute that is a MappingAttribute (which should include all attributes, as stated in the assumptions). The result would be meaningless if there were displacements in attribute positions.
//...
var Index: Integer := 0;
var Found: Boolean := False;
for var Attr: TCustomAttribute in RTTIType.GetAttributes do
begin
if Attr is MappingAttribute then
begin
Inc(Index);
Found := MappingAttribute(Attr).Value = Value;
if Found then
break;
end;
end;
if not Found then
raise Exception.CreateFmt('No match for %s on %s', [Value, RTTIType.Name]);
//...
If the attribute is not found, the function raises an exception to prevent the execution of dependent operations. Otherwise, it proceeds to return the value.
Now, returning the value becomes slightly more complex because I didn’t want to return a TValue; I wanted to return the same type T. Since I don’t have much experience with generics, the method I found to return T (which is essentially an integer) involves getting the address of the Result variable, casting it to a pointer to a byte, dereferencing it, and assigning Index to it.
//...
PByte(@Result)^ := Index;
Exit(Result);
//...
And it concludes the second function.
Conclusion
My intention with this article was to demonstrate a way to have enums that are intrinsically mapped to strings and are accessible both ways (enum to string and string to enum). I believe that throughout the text it becomes clear that Delphi is not dead at all; on the contrary, it’s constantly evolving by adding new features and improving existing ones. It’s a powerful language, and when combined with the RAD Studio interface, it allows for the fast development of robust, reliable, and lightweight applications. Delphi also offers means to directly interact with memory, leveraging the language’s use cases. Furthermore, Delphi’s strong community support (which helped me with this issue too) and comprehensive tooling make it an excellent choice for both beginners and experienced developers. Its seamless integration with modern technologies ensures that developers can build cutting-edge applications efficiently. Whether you’re developing desktop applications, mobile apps, or server-side solutions, Delphi provides the flexibility and performance needed to meet diverse project requirements. This continued evolution and versatility affirm Delphi’s enduring relevance in the software development landscape, empowering developers to solve complex problems with elegance and efficiency.
