Getting started with Cirrus

Oxygene's AOP 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 be used from both Oxygene, C# and Swift projects, on all three target platforms.

.NET Only

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

Writing an Aspect

You have to create a new class library, reference the RemObjects.Elements.Cirrus library, and create a new attribute class. This class should descend from System.Attribute and have a 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 ClassLibrary1;

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 ClassLibrary1
{
    [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 ClassLibrary1;

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());
            }
        });
    }
}

In the 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 AutoInjectIntoClass is an aspect 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
  ClassLibrary1,
  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 ClassLibrary1;
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);
    }
  }

}

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

*Note': Because aspects are applied at compile time, the final executable will not depend on the aspect library or on Cirrus anymore.

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 is been injected into our class, and the reference section lists only mscorlib - RemObjects.Elements.Cirrus.dll and our aspect .dll do not need to be deployed for the application to run.