Code Generation
The TMS BCL code generation framework allows you to build Delphi source code programmatically. It follows a two-step approach:
- Build a code DOM (Document Object Model) — an in-memory object graph that represents the structure of a Delphi unit.
- Generate source code — pass the DOM to TDelphiCodeGenerator to produce formatted Delphi source code as a string.
The code DOM classes are declared in the Bcl.Code.MetaClasses unit. The generator is declared in the Bcl.Code.DelphiGenerator unit.
Code DOM Overview
The code DOM is a hierarchy of objects rooted at TCodeUnit, which represents a complete Delphi unit. All code elements inherit from the TCodeObject base class. The main classes are:
- TCodeUnit — A complete Delphi unit with uses clauses, type declarations, functions, variables, and initialization/finalization sections.
- TCodeTypeDeclaration — A type declaration: class, record, interface, or enumeration.
- TCodeMemberField — A field declaration within a type.
- TCodeMemberMethod — A method (procedure or function) within a type.
- TCodeMemberConstructor — A constructor declaration.
- TCodeMemberDestructor — A destructor declaration.
- TCodeMemberProperty — A property declaration within a type.
- TCodeMemberConst — A constant declaration.
- TCodeTypeAliasDeclaration — A type alias (e.g.,
TMyType = TSomeOtherType). - TCodeAttributeDeclaration — A custom attribute applied to a code element.
Statements within method bodies are represented by TCodeStatement descendants such as TCodeSnippetStatement, TCodeConditionStatement, TCodeForStatement, TCodeForEachStatement, TCodeWhileStatement, TCodeTryFinallyStatement, and TCodeTryExceptStatement.
Building a Unit
Create a TCodeUnit instance, set its name, and add units to the uses clause with TCodeUnit.UseUnit:
CodeUnit := TCodeUnit.Create;
try
CodeUnit.Name := 'MyModule';
CodeUnit.UseUnit('SysUtils');
CodeUnit.UseUnit('Classes');
CodeUnit.UseUnit('Generics.Collections', True); // Add to implementation uses
finally
CodeUnit.Free;
end;
The UseUnit method avoids duplicates automatically. If a unit is already in the interface uses clause, calling UseUnit again has no effect. Pass True as the second parameter to add a unit to the implementation uses clause instead.
Declaring Types
Classes
Set TCodeTypeDeclaration.IsClass to True and assign the ancestor type through TCodeTypeDeclaration.BaseType:
CodeUnit := TCodeUnit.Create;
try
CodeUnit.Name := 'MyModule';
MyClass := TCodeTypeDeclaration.Create;
MyClass.Name := 'TCustomer';
MyClass.IsClass := True;
MyClass.BaseType.BaseType := 'TObject';
CodeUnit._Types.Add(MyClass);
finally
CodeUnit.Free;
end;
Add the type to the unit through the TCodeUnit._Types list. The unit owns the type and frees it when destroyed.
Records
Set TCodeTypeDeclaration.IsRecord to True:
CodeUnit := TCodeUnit.Create;
try
CodeUnit.Name := 'MyModule';
MyRecord := TCodeTypeDeclaration.Create;
MyRecord.Name := 'TPoint3D';
MyRecord.IsRecord := True;
MyRecord.AddField('X', 'Double', mvPublic);
MyRecord.AddField('Y', 'Double', mvPublic);
MyRecord.AddField('Z', 'Double', mvPublic);
CodeUnit._Types.Add(MyRecord);
finally
CodeUnit.Free;
end;
Interfaces
Set TCodeTypeDeclaration.IsInterface to True and assign a base interface through TCodeTypeDeclaration.BaseType:
CodeUnit := TCodeUnit.Create;
try
CodeUnit.Name := 'MyModule';
Intf := TCodeTypeDeclaration.Create;
Intf.Name := 'ILogger';
Intf.IsInterface := True;
Intf.BaseType.BaseType := 'IInterface';
Intf.AddProcedure('Log', mvPublic).AddParameter('Msg', 'string');
Intf.AddFunction('GetLevel', 'Integer', mvPublic);
CodeUnit._Types.Add(Intf);
finally
CodeUnit.Free;
end;
For interfaces with a GUID, set the TCodeTypeDeclaration.InterfaceGuid property.
Enumerations
Set TCodeTypeDeclaration.IsEnum to True and add members by name. Each member is a TCodeTypeMember with only its Name set:
CodeUnit := TCodeUnit.Create;
try
CodeUnit.Name := 'MyModule';
EnumType := TCodeTypeDeclaration.Create;
EnumType.Name := 'TOrderStatus';
EnumType.IsEnum := True;
EnumType.Members.Add(TCodeTypeMember.Create);
EnumType.Members.Last.Name := 'osPending';
EnumType.Members.Add(TCodeTypeMember.Create);
EnumType.Members.Last.Name := 'osProcessing';
EnumType.Members.Add(TCodeTypeMember.Create);
EnumType.Members.Last.Name := 'osCompleted';
CodeUnit._Types.Add(EnumType);
finally
CodeUnit.Free;
end;
Adding Members
TCodeTypeDeclaration provides convenience methods for adding fields, properties, methods, constructors, and destructors. Each method returns the created instance for further configuration:
MyClass := TCodeTypeDeclaration.Create;
try
MyClass.Name := 'TCustomer';
MyClass.IsClass := True;
MyClass.BaseType.BaseType := 'TObject';
// Add private fields
MyClass.AddField('FName', 'string', mvPrivate);
MyClass.AddField('FAge', 'Integer', mvPrivate);
MyClass.AddField('FActive', 'Boolean', mvPrivate);
// Add a constructor and destructor
MyClass.AddConstructor;
MyClass.AddDestructor;
// Add a procedure
Method := MyClass.AddProcedure('Validate', mvPublic);
Method.Directives := [mdVirtual];
// Add a function
Method := MyClass.AddFunction('ToString', 'string', mvPublic);
Method.Directives := [mdOverride];
// Add properties
MyClass.AddProperty('Name', 'string', 'FName', 'FName', mvPublic);
MyClass.AddProperty('Age', 'Integer', 'FAge', 'FAge', mvPublic);
// Add a read-only property
Prop := MyClass.AddProperty('Active', 'Boolean', 'FActive', '', mvPublic);
Prop.HasSetter := False;
finally
MyClass.Free;
end;
Member Visibility
All members accept a TMemberVisibility parameter that maps to Delphi visibility sections:
| Value | Delphi Section |
|---|---|
mvPrivate |
private |
mvProtected |
protected |
mvPublic |
public |
mvPublished |
published |
mvStrictPrivate |
strict private |
mvStrictProtected |
strict protected |
Method Directives
Use the TCodeMemberMethod.Directives property to apply method directives. The available directives in TCodeMethodDirective are: mdVirtual, mdDynamic, mdReintroduce, mdOverload, mdOverride, mdAbstract, and mdStatic.
Method Bodies
Add statements to a method body using TCodeMemberMethod.AddSnippet for raw code lines or TCodeMemberMethod.AddSnippetFmt for formatted strings. Declare local variables with TCodeMemberMethod.DeclareVar:
MyClass := TCodeTypeDeclaration.Create;
try
MyClass.Name := 'TCustomer';
MyClass.IsClass := True;
Method := MyClass.AddFunction('GetDisplayName', 'string', mvPublic);
Method.AddParameter('AIncludeAge', 'Boolean');
// Declare local variables
Method.DeclareVar('Suffix', 'string');
// Add statements using code snippets
Method.AddSnippet('Suffix := ''''');
Method.AddSnippetFmt('if %s then', ['AIncludeAge']);
Method.AddSnippetFmt(' Suffix := '' ('' + IntToStr(%s) + '')''', ['FAge']);
Method.AddSnippetFmt('Result := %s + Suffix', ['FName']);
finally
MyClass.Free;
end;
Structured Statements
For structured control flow, use the dedicated statement classes instead of raw snippets. These classes produce properly formatted output when the generator's TDelphiCodeGenerator.StructureStatements property is enabled:
Method := TCodeMemberMethod.Create;
try
// if..then..else statement
CondStmt := TCodeConditionStatement.Create('FAge >= 18');
CondStmt.TrueStatements.AddSnippet('Result := ''Adult''');
CondStmt.FalseStatements.AddSnippet('Result := ''Minor''');
Method.Statements.Add(CondStmt);
// for loop statement
ForStmt := TCodeForStatement.Create(
'I',
TCodeSnippetExpression.Create('0'),
TCodeSnippetExpression.Create('Count - 1'),
[TCodeSnippetStatement.Create('ProcessItem(I)')]
);
Method.Statements.Add(ForStmt);
// try..finally statement
TryStmt := TCodeTryFinallyStatement.Create(
[TCodeSnippetStatement.Create('Stream := TFileStream.Create(FileName, fmOpenRead)')],
[TCodeSnippetStatement.Create('Stream.Free')]
);
Method.Statements.Add(TryStmt);
finally
Method.Free;
end;
The available structured statement classes are:
- TCodeConditionStatement —
if..then..else - TCodeForStatement —
for..to/for..downto(set Descending toTruefor downto) - TCodeForEachStatement —
for..in - TCodeWhileStatement —
while..do - TCodeTryFinallyStatement —
try..finally - TCodeTryExceptStatement —
try..except
Parameter Modifiers
When adding parameters to a method via TCodeMemberMethod.AddParameter, the default modifier is pmNone (value parameter). To create var or const parameters, set the TCodeParameterDeclaration.Modifier property to pmVar or pmConst. Default values can be assigned through the TCodeParameterDeclaration.DefaultValue property.
Attributes and Comments
Custom Attributes
Add attributes to types and members using the AddAttribute method. Use TCodeAttributeDeclaration.AddRawArgument to pass arguments. The method returns the attribute instance for chaining:
MyClass := TCodeTypeDeclaration.Create;
try
MyClass.Name := 'TCustomer';
MyClass.IsClass := True;
// Add attribute to the type
MyClass.AddAttribute('Entity');
MyClass.AddAttribute('Table').AddRawArgument('''customers''');
// Add attribute to a field
Field := MyClass.AddField('FName', 'string', mvPrivate);
Field.AddAttribute('Column').AddRawArgument('''customer_name''');
finally
MyClass.Free;
end;
Comments
Add comments to any type member through the TCodeTypeMember.Comments list. The TCodeComment class supports three styles via TCommentStyle:
- csSingleLine —
// comment - csBlock —
{ comment } - csDocumentation —
/// comment
Generating Source Code
Pass the completed TCodeUnit to TDelphiCodeGenerator.GenerateCodeFromUnit to produce the Delphi source code:
CodeUnit := TCodeUnit.Create;
try
CodeUnit.Name := 'MyModule';
// ... build the unit ...
Generator := TDelphiCodeGenerator.Create;
try
Source := Generator.GenerateCodeFromUnit(CodeUnit);
// Source now contains the complete Delphi source code
finally
Generator.Free;
end;
finally
CodeUnit.Free;
end;
Generator Options
TDelphiCodeGenerator provides several properties to control the output format:
| Property | Description |
|---|---|
| SortMembers | Sort type members alphabetically by name. |
| SortUnits | Sort units in the uses clause alphabetically. |
| LineBeforeComplexMembers | Insert a blank line before members that have comments or attributes. |
| StructureStatements | Output structured statements with proper formatting and semicolons. |
| ReservedWordMode | Control how Delphi reserved words used as identifiers are handled. |
Reserved Word Handling
When an identifier matches a Delphi reserved word, the generator can adjust it based on the TReservedWordMode setting:
- Underline — Appends an underscore (e.g.,
typebecomestype_). - Ampersand — Prefixes with ampersand (e.g.,
typebecomes&type). - Ignore — Outputs the identifier unchanged.
Use TDelphiCodeGenerator.IsReservedWord to check whether a string is a Delphi reserved word.
Complete Example
The following example builds a complete unit with a class, constructor, method with local variables, and properties, then generates the source code:
CodeUnit := TCodeUnit.Create;
try
CodeUnit.Name := 'CustomerModel';
CodeUnit.UseUnit('SysUtils');
CodeUnit.UseUnit('Classes');
// Create the TCustomer class
Customer := TCodeTypeDeclaration.Create;
Customer.Name := 'TCustomer';
Customer.IsClass := True;
Customer.BaseType.BaseType := 'TPersistent';
...
// Add fields
Customer.AddField('FName', 'string', mvPrivate);
Customer.AddField('FEmail', 'string', mvPrivate);
Customer.AddField('FAge', 'Integer', mvPrivate);
// Add constructor with body
Method := Customer.AddConstructor;
Method.AddParameter('AName', 'string');
Method.AddParameter('AEmail', 'string');
Method.AddSnippet('inherited Create');
Method.AddSnippet('FName := AName');
Method.AddSnippet('FEmail := AEmail');
...
// Add a function with local variables and logic
Method := Customer.AddFunction('GetSummary', 'string', mvPublic);
Method.DeclareVar('AgeStr', 'string');
Method.AddSnippet('AgeStr := IntToStr(FAge)');
Method.AddSnippet('Result := FName + '' <'' + FEmail + ''> (Age: '' + AgeStr + '')''');
// Add properties
Customer.AddProperty('Name', 'string', 'FName', 'FName', mvPublished);
Customer.AddProperty('Email', 'string', 'FEmail', 'FEmail', mvPublished);
Customer.AddProperty('Age', 'Integer', 'FAge', 'FAge', mvPublished);
CodeUnit._Types.Add(Customer);
...
// Generate the source code
Generator := TDelphiCodeGenerator.Create;
try
Generator.SortMembers := True;
Generator.StructureStatements := True;
Result := Generator.GenerateCodeFromUnit(CodeUnit);
finally
Generator.Free;
end;
finally
CodeUnit.Free;
end;
This is the generated source code output:
unit CustomerModel;
interface
uses
SysUtils,
Classes;
type
TCustomer = class;
TCustomer = class(TPersistent)
private
FAge: Integer;
FEmail: string;
FName: string;
public
constructor Create(AName: string; AEmail: string);
function GetSummary: string;
published
property Age: Integer read FAge write FAge;
property Email: string read FEmail write FEmail;
property Name: string read FName write FName;
end;
implementation
{ TCustomer }
constructor TCustomer.Create(AName: string; AEmail: string);
begin
inherited Create;
FName := AName;
FEmail := AEmail;
end;
function TCustomer.GetSummary: string;
var
AgeStr: string;
begin
AgeStr := IntToStr(FAge);
Result := FName + ' <' + FEmail + '> (Age: ' + AgeStr + ')';
end;
end.