Getting started with Cirrus

Elements' AOP systen, Cirrus, makes it possible to change the behavior of code, add or remove fields, properties, events or methods and even extra classes, by applying special kinds of attributes - Aspects - to classes or members.

Aspects are written in Elements itself, compiled into a separate library, and are reusable by different projects. They are fairly simple to write. Aspects can created using any of the Object Oriented Elements languages, using .NET, and can used from Oxygene, C#, Swift, Java and Meerciry, on all target platforms.

Aspects Get Implemented in .NET Only

While aspects can be used on all three platfroms, but they can be only implemented using .NET.

the Cirrus core library is build on .NET Standard 2.0, so that it can be used both with Classic .NET and .NET Core. Aspects must be compiled for Classic .NET (because that is what the compiler runs on), version 4.8 (not lower) or .NET Standard 2.0 (not higher).

Writing an Aspect

To write an aspect, simply create a new .NET (Classic) Class Library and set its Target Framework to 4.8, or a new .NET Standard Class Library and set its Target Framework to 2.0. Then add a reference the RemObjects.Elements.Cirrus library shipping with Elements, via the regular Add Reference dialog. Finally add a new class descending from System.Attribute that featres the regular AttributeUsage() attribute to denote where it can be applied. The only difference from a regular attribute is that aspects implement one of the special interfaces defined by Cirrus, such as IMethodImplementationDecorator, as in the sample below.

Aspect attributes are loaded and instantiated by the compiler at compile time, and are given the chance to take very powerful influence on the code the compiler is generating.

In the example below, we are creating an aspect to decorate methods of the class it is applied to. This is done through the IMethodImplementationDecorator interface, which requires one single method, HandleImplementation to be implemented by the aspect. The compiler will call this method after a method body (implementation) was generated and allows the aspect to take influence on the generated code and to change or augment it:

namespace MyAspectLibrary;

interface

uses
  RemObjects.Elements.Cirrus;

type
  [AttributeUsage(AttributeTargets.Class or AttributeTargets.Struct)]
  LogToMethodAttribute = public class(System.Attribute, IMethodImplementationDecorator)
  public
    [AutoInjectIntoTarget]
    class method LogMessage(aEnter: Boolean; aName: String; Args: Array of object);

    method HandleImplementation(Services: IServices; aMethod: IMethodDefinition);
  end;

implementation

class method LogToMethodAttribute.LogMessage(aEnter: Boolean; aName: String;
  Args: Array of object);
begin
  if aEnter then
    Console.WriteLine('Entering ' + aName)
  else
    Console.WriteLine('Exiting ' + aName);
end;

method LogToMethodAttribute.HandleImplementation(Services: IServices;
  aMethod: IMethodDefinition);
begin
  if String.Equals(aMethod.Name, 'LogMessage', StringComparison.OrdinalIgnoreCase) then exit;
  if String.Equals(aMethod.Name, '.ctor', StringComparison.OrdinalIgnoreCase) then exit;

  aMethod.SetBody(Services,
    method begin
      LogMessage(true, Aspects.MethodName, Aspects.GetParameters);
      try
        Aspects.OriginalBody;
      finally
        LogMessage(false, Aspects.MethodName, Aspects.GetParameters);
      end;
    end);
end;

end.
using RemObjects.Elements.Cirrus;

namespace MyAspectLibrary
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class LogToMethodAttribute: System.Attribute, IMethodImplementationDecorator
    {
        [AutoInjectIntoTarget]
        public static void LogMessage(bool aEnter, String aName, object[] Args)
        {
            if (aEnter)
                Console.WriteLine("Entering " + aName);
            else
                Console.WriteLine("Exiting " + aName);
        }

        public void HandleImplementation(IServices Services, IMethodDefinition aMethod)
        {
            if (String.Equals(aMethod.Name, "LogMessage", StringComparison.OrdinalIgnoreCase)) return;
            if (String.Equals(aMethod.Name, ".ctor", StringComparison.OrdinalIgnoreCase)) return;
            
            aMethod.SetBody(Services, (services, meth) => {
                LogMessage(true, Aspects.MethodName(), Aspects.GetParameters());
                try
                {
                    Aspects.OriginalBody();
                }
                finally
                {
                    LogMessage(false, Aspects.MethodName(), Aspects.GetParameters());
                }
            });
        }
    }
}
import RemObjects.Elements.Cirrus

@AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)
public class LogToMethodAttribute : System.Attribute, IMethodImplementationDecorator
{
    @AutoInjectIntoTarget
    public static func LogMessage(_ aEnter: Bool, _ aName: String, _ aArgs: object[])
    {
        if (aEnter) {
            Console.WriteLine("Entering " + aName)
        } else {
            Console.WriteLine("Exiting " + aName)
        }
    }

    public func HandleImplementation(_ Services: IServices, _ aMethod: IMethodDefinition)
    {
        if String.Equals(aMethod.Name, "LogMessage", StringComparison.OrdinalIgnoreCase) { 
            return 
        }
        if String.Equals(aMethod.Name, ".ctor", StringComparison.OrdinalIgnoreCase) { 
            return 
        }
            
        aMethod.SetBody(Services) { (services, meth) in

            LogMessage(true, Aspects.MethodName(), Aspects.GetParameters())
            defer {
                LogMessage(false, Aspects.MethodName(), Aspects.GetParameters())
            }

            do {
                Aspects.OriginalBody()
            }
        }
    }
}
package MyAspectLibrary;

import RemObjects.Elements.Cirrus.*;

@AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)
public class LogToMethodAttribute extends System.Attribute implements IMethodImplementationDecorator
{
    @AutoInjectIntoTarget
    public static void LogMessage(bool aEnter, String aName, object[] Args)
    {
        if (aEnter)
            Console.WriteLine("Entering " + aName);
        else
            Console.WriteLine("Exiting " + aName);
    }

    public void HandleImplementation(IServices Services, IMethodDefinition aMethod)
    {
        if (String.Equals(aMethod.Name, "LogMessage", StringComparison.OrdinalIgnoreCase)) return;
        if (String.Equals(aMethod.Name, ".ctor", StringComparison.OrdinalIgnoreCase)) return;
            
        aMethod.SetBody(Services, (services, meth) => {
            LogMessage(true, Aspects.MethodName(), Aspects.GetParameters());
            try
            {
                Aspects.OriginalBody();
            }
            finally
            {
                LogMessage(false, Aspects.MethodName(), Aspects.GetParameters());
            }
        });
    }
}
Imports RemObjects.Elements.Cirrus

<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Struct)>
Public Class LogToMethodAttribute
  Inherits System.Attribute
  Implements IMethodImplementationDecorator

  <AutoInjectIntoTarget>
  Public Shared Sub LogMessage(aEnter As Boolean, aName As [String], Args As Object())
    If aEnter Then
      Console.WriteLine("Entering " + aName)
    Else
      Console.WriteLine("Exiting " + aName)
    End If
  End Sub

  Public Sub HandleImplementation(Services As IServices, aMethod As IMethodDefinition)
    If String.Equals(aMethod.Name, "LogMessage", StringComparison.OrdinalIgnoreCase) Then
      Return
    End If
    If String.Equals(aMethod.Name, ".ctor", StringComparison.OrdinalIgnoreCase) Then
      Return
    End If
    aMethod.SetBody(Services, Sub(aServices2, meth)
      LogMessage(true, Aspects.MethodName(), Aspects.GetParameters())
      Try
        Aspects.OriginalBody()
      Finally
        LogMessage(false, Aspects.MethodName(), Aspects.GetParameters())
      End Try
    End Sub)
  End Sub

End Class

In the code fragment above, our aspect compares the method name to ".ctor" and "LogMessage" (we do not want to augment those), and if they do not match, it adds LogMessage calls around the original method, protected by a try/finally.

The Aspects class is a special Compiler Magic Class provided by Cirrus that allows the aspect to take control of the code it is being applied to. Among other things, you see that it can query for the method name and the parameters, but also the body of the method in question, as written in the original source for the class.

By calling SetBody() on the method, the aspect can replace the body of the generated code (in this case, by taking the original body and surrounding our calls to LogMessage). Note how the new method body is being provided as plain, readable Oxygene code, in form of an extension to the anonymous method syntax.

It is also worth noting that the LogMessage method of our aspect has an aspect of its own applied. The AutoInjectIntoTarget Aspect is defined by Cirrus itself, and it's intended for use within aspects only. It causes the member (in this case the LogMessage method) to be added to the class the aspect is applied to.

This is necessary since our aspect makes use of LogMessage() in the new and augmented method body - but no such method is likely to exist in the target object. Without AutoInjectIntoClass, all the logic for LogMessage would need to be crammed into the SetBody call - making it potentially harder to read, but also potentially duplicating a lot of code and logic.

The following application makes use of our Log aspect. Note how this can be done in both Oxygene and RemObjects C#.

namespace CirrusTest;

interface

uses
  MyAspectLibrary,
  System.Linq;


type
  [aspect:LogToMethod]
  ConsoleApp = class
  public
    class method Main;
    class method Test(S: string);
  end;

implementation

class method ConsoleApp.Main;
begin
  Console.WriteLine('Hello World.');
  Test('Input for Test');
end;
class method ConsoleApp.Test(S: string);
begin
  Console.WriteLine('TEST: '+s);
end;

end.
using MyAspectLibrary;
using System.Linq;

namespace CirrusTest
{

  [__aspect:LogToMethod]
  public class ConsoleApp
  {
    public static void Main()
    {
      Console.WriteLine('Hello World.');
      Test('Input for Test');
    }

    public static void Test(string S)
    {
      Console.WriteLine('TEST: '+s);
    }
  }

}
Imports MyAspectLibrary

<LogToMethod>
Public Class ConsoleApp

  Public Shared Sub Main()
    Console.WriteLine(Null)
    Test(Null)
  End Sub

  Public Shared Sub Test(S As String)
    Console.WriteLine(Null)
  End Sub

End Class

We simply created a new console app that references the aspect library we created above, as well as the Cirrus library.

<div class="note"? *Note': Because aspects are applied at compile time, the final executable will not depend on the aspect library or on Cirrus anymore.

This is also what enabled Aspects – although written in .NET – to be used in projects for any platform.

Running and debugging this program will output a log message at the beginning and end of each method, just as specified in our designed aspect.

Entering Main
Hello World.
Entering Test
TEST: Input for Test
Exiting Test
Exiting Main

When this code is run, the LogMessage method has been injected into our class, and the binary will nit reference or require RemObjects.Elements.Cirrus.dll or our aspect .dll to run.