Metaprogramação
late modules, late begin, meta modules, atributos e inject — a compilação como parte do programa.
Em Fenner o compilador não é uma caixa-preta: o programa pode olhar para si mesmo depois de compilado e acrescentar código. São três ferramentas, em ordem crescente de poder.
module → marca de código morto → late module → meta → late meta moduleLate module#
Um late module é lido depois de o programa inteiro existir e de o compilador marcar o que sobreviveu à eliminação de código não usado. O código normal não enxerga nomes dele (quando os outros olharam, ele ainda não existia); ele fala com o programa pelo que registra: um ponteiro de função preenchido na initialization dele, que roda por último.
program Main;
uses AppCore, AppLate;
var
U: TUsed;
begin
U := TUsed.Create;
WriteLn('1: ', U.Double(21));
// o late module se registrou na initialization dele, que roda por último
WriteLn('2: ', CallRegistered);
end.module AppCore;
// O núcleo: declara um tipo que o programa USA (TUsed), um que ninguém usa
// (TUnused, que a eliminação de código morto descarta) e o ponto de registro
// que o late module preenche.
public type
TRegistrar = procedure;
public type
TUsed = class(TObject)
public
function Double(A: Int32): Int32;
end class;
public type
TUnused = class(TObject)
public
function Never: Int32;
end class;
public var
GRegister: TRegistrar;
public var
GStatus: UString;
public function CallRegistered: UString;
begin
if GRegister <> nil then
begin
GRegister();
Exit(GStatus);
end if;
Exit('ninguém registrou');
end function;
function TUsed.Double(A: Int32): Int32;
begin
Exit(A * 2);
end function;
function TUnused.Never: Int32;
begin
Exit(0);
end function;
end.late module AppLate;
// Um late module é lido DEPOIS de o programa inteiro existir e de a eliminação
// de código não usado marcar o que sobreviveu. Declared(X) responde se X
// sobreviveu; citar um símbolo morto seria erro.
uses AppCore;
private procedure ReportSurvivors;
begin
{$if Declared(TUsed)}
GStatus := 'TUsed sobreviveu';
{$else}
GStatus := 'TUsed sumiu?!';
{$endif}
{$if Declared(TUnused)}
GStatus := GStatus + ', TUnused também';
{$else}
GStatus := GStatus + ', TUnused foi descartada';
{$endif}
end procedure;
initialization
GRegister := @ReportSurvivors;
end.1: 42
2: TUsed sobreviveu, TUnused foi descartadaDeclared(X)responde seXsobreviveu. Citar um símbolo morto é erro; proteja com{$if Declared(X)}.- A
initializationdo late module roda por último, e afinalization, primeiro. - Nesse exemplo,
TUnusednão é usada por ninguém vivo: dentro do late module,Declared(TUnused)respondeFalse.
late begin … end dentro de rotina#
O mesmo adiamento, em escala de comando:
procedure Register(const APrefix: UString);
var
LLocal: Int32;
begin
LLocal := 40;
late begin
// só é ANALISADO depois da marcação, e enxerga o parâmetro e o local
GLog := APrefix + IntToUStr(LLocal + 2);
{$if Declared(TCustomer)}
Registers(TCustomer);
{$endif}
end;
end procedure;O bloco é capturado como texto (as diretivas de dentro só valem na fase late), e o nó vazio já fica no lugar certo da árvore, então a ordem dos comandos é a escrita. Ele enxerga parâmetros, locais, campos da classe e os nomes do módulo onde foi escrito. late não é palavra reservada: só colado num begin. Fora de uma rotina é erro.
Meta module#
Um meta module não entra no programa: ele vira um programa temporário que roda em compilação, lê o retrato do que foi compilado (módulos, tipos, atributos, a marca de morto) e devolve edições: métodos novos e emendas em corpos existentes, aplicados num segundo passe.
program Main;
uses Entities, TableLabel;
var
C: TCustomer;
begin
C := TCustomer.Create;
C.Name := 'maria';
WriteLn('nome: ', C.Name);
// Describe não existe em fonte nenhum: o meta module o criou em compilação
WriteLn('descrição: ', C.Describe);
end.module Entities;
// Uma classe decorada com um atributo. O método Describe NÃO existe aqui:
// quem o cria é o meta module, rodando em compilação sobre o retrato do programa.
public attribute DbTable(const TableName: LString);
public type
[DbTable('CUSTOMERS')]
TCustomer = class(TObject)
public
Name: UString;
end class;
end.meta module TableLabel;
// Roda em COMPILAÇÃO: varre o retrato do programa e, em toda classe viva com o
// atributo DbTable, acrescenta um método Describe que devolve o nome da tabela.
// O módulo Meta da RTL (a árvore TMetaRtti* e o TMetaMethodBuilder) entra implícito.
private procedure Execute;
var
Ctx: TMetaRttiContext;
Pack: TMetaRttiPackage;
Modl: TMetaRttiModule;
TypeInfo: TMetaRttiType;
Attr: TMetaRttiAttribute;
Table: UString;
Builder: TMetaMethodBuilder;
begin
for Pack in Ctx.GetPackages() do
for Modl in Pack.GetModules() do
if (not Modl.IsMeta()) and (not Modl.IsLate()) then
for TypeInfo in Modl.GetTypes() do
if (not TypeInfo.IsDead()) and TypeInfo.IsClass() then
begin
Table := '';
for Attr in TypeInfo.GetAttributes() do
if Attr.TypeNameIs('DbTable') then
Table := Attr.ValueDef('TableName');
if Length(Table) > 0 then
begin
Builder := TMetaMethodBuilder.Create(TypeInfo, 'Describe');
if not Builder.Exist() then
begin
Builder.Start();
Builder.SetResult('UString');
Builder.SetSourceText(' Exit(''tabela ' + Table + ''');');
end if;
end if;
end if;
end procedure;
initialization
Execute;
end.nome: maria
descrição: tabela CUSTOMERSO método Describe não existe em fonte nenhum: o TableLabel o cria lendo o atributo DbTable no retrato do programa, e o Main o chama como se sempre tivesse existido.
- O módulo
Metada RTL (a árvoreTMetaRtti*e oTMetaMethodBuilder) entra implícito nesse mundo. - Um
late meta moduleé um late que carrega depois dos metas, e enxerga o que eles geraram. - É assim que se faz serialização, mapeamento para banco de dados e outros códigos repetitivos sem reflexão em execução: o custo é pago na compilação, e o binário só tem o código final.
Atributos#
attribute Nome(params); declara a forma de um atributo, e [Nome(args)] decora. Os atributos são lidos pelo compilador, conferidos contra a declaração e gravados só no RTTI externo, e é isso que alimenta os meta modules. Veja Classes e helpers.
inject: pedaços de método e macros#
inject into procedure X segment N; begin … end inject; declara um pedaço que entra no método X. A classe que não escreve X ganha um override gerado (inherited mais os pedaços, por segmento); a que escreve marca o lugar com Hook(N);. O %_CurrentType_% é a classe que expandiu a macro, o que deixa o pedaço citar o próprio tipo.
É o mecanismo de aspectos: uma macro declara um campo e, no mesmo gesto, o código de Limpa e de Copia que o trata, e cada classe que usa a macro ganha tudo sem repetir nada. Os pedaços só são colados no fim, quando todos já foram lidos, então a ordem de escrita não importa.