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 be created using any of the Object Oriented Elements languages, using .NET, and can used from Oxygene, C#, Swift, Java and Mercury, 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
, and optionally 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.
*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.