Fenner
A linguagem

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.

texto
module  →  marca de código morto  →  late module  →  meta  →  late meta module

Late 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.

Main.fxp
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.
AppCore.fxm
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.
AppLate.fxm
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.
Saída
1: 42
2: TUsed sobreviveu, TUnused foi descartada
  • Declared(X) responde se X sobreviveu. Citar um símbolo morto é erro; proteja com {$if Declared(X)}.
  • A initialization do late module roda por último, e a finalization, primeiro.
  • Nesse exemplo, TUnused não é usada por ninguém vivo: dentro do late module, Declared(TUnused) responde False.

late begin … end dentro de rotina#

O mesmo adiamento, em escala de comando:

Fenner
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.

Main.fxp
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.
Entities.fxm
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.
TableLabel.fxm
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.
Saída
nome:     maria
descrição: tabela CUSTOMERS

O 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 Meta da RTL (a árvore TMetaRtti* e o TMetaMethodBuilder) 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.

Quando usar cada um#

Quer…Use
escolher código pelo tipo de um genérico{$if} (Genéricos)
repetir um texto com variações{$macro} (Diretivas)
que um plugin se adapte ao que sobreviveulate module
gerar métodos a partir de atributos e tiposmeta module
acrescentar o mesmo código a muitas classesinject