Feature request: simpler programming of aspects

@ck, @mh, I am now working (struggling) a while with aspects, and the biggest problem is to generate code - generating a line of code is not something you easily do within Cirrus; generating a complete method becomes really time consuming to get it right.

I know that the reason for this is to be language independent; the aspect can be applied on any RemObjects language because of this type library as the code is generated from it.

But I was thinking that this could be much easier to implement.

If I was able to create static methods without any dependencies in a separate code unit within the aspect dll , that you program in the language that is used to create the aspect, this code could be compiled into a separate code unit that is linked to the project where the aspect is applied.

To do so, the code has to be compiled into the aspect dll as plain text.

I take the ExtensionField code as an example.
In this code I can add the following unit:

namespace builditAspects;

[ApplyOnAspect(ExtensionField)]
type ExtensionFieldsMethods<S, V> = public static class
    method GetKeyOnTarget(findKey: S; Dictionary: System.Collections.Generic.Dictionary<WeakReference, V>);
    begin
        var ToRemove := new System.Generic.List(System.WeakReference);
        For each k: System.WeakReference in Test_Dict.Keys do
        begin
            if k.Target = self then
            begin
                result := k;
                break;
            end
            else
                if k.Target = nil then                                      
                    ToRemove.Add(k);
        end;
        For each k1: System.WeakReference in ToRemove do
            Test_Dict.Remove(k1);
    end;

    method Get_Field_Value(key: S; Dictionary: System.Collections.Generic.Dictionary<WeakReference, V>): Object;
    begin
        var key := GetKeyOnTarget(key, Dictionary);
        if key <> nil then
            exit Test_Dict[key]
        else
            exit default;
    end;

    method Set_Field_Value(key: S; val: V; Dictionary: System.Collections.Generic.Dictionary<WeakReference, V>);
    begin
        var key := GetKeyOnTarget(key, Dictionary);
        if key <> nil then
            Dictionary.Keys[key] := val
        else if val <> nil then
            Dictionary.Add(self, val); 
    end;
end;

end.

The attribute [ApplyOnAspect(ExtensionField)] is telling the compiler to save a plain text cody of this code and compile it as a resource in the dll with the name ExtensionField.res.

Then I build the aspect - which is now much simpler:

namespace builditAspects;

interface

uses 
  System.Linq,
  RemObjects.Elements.Cirrus.*;

type
    [AttributeUsage(AttributeTargets.Field)]
    ExtensionField = public class(Attribute, IFieldInterfaceDecorator)
private
public
    method HandleInterface(Services: IServices; aField: IFieldDefinition);
end;

implementation

method ExtensionField.HandleInterface(Services: IServices; aField: IFieldDefinition);
begin
//Needed in all of the code
    var SelfType := aField.Owner.ExtensionTypeFor;
    var WeakReferenceType :=  Services.FindType("System.WeakReference");
    var Methods := new TypeValue(Services.FindType("builditAspects.ExtensionFieldsMethods`2"));

//Add the private storage dictionary
    var t := Services.FindType("System.Collections.Generic.Dictionary`2");
    var t1 := Services.CreateGenericInstance(t, WeakReferenceType, aField.Type);  //initialize the generic parameters of the dictionary                                        
    var StorageField := aField.Owner.AddField(aField.Name + "_Dict", t1, true);
    StorageField.Visibility := Visibility.Private;

    // -> this part does not work; no assignment is generated (also no error is raised)
    StorageField.InitialValue := new NewValue(t1);
    if aField.Owner.GetClassConstructor = nil then aField.Owner.AddConstructor(true);

//private write method for field replacing property 
    var lWrite := aField.Owner.AddMethod('set_' + aField.Name, nil,  aField.Static);
    lWrite.AddParameter('self', ParameterModifier.In, SelfType).Prefix := '$mapped';
    lWrite.AddParameter('val', ParameterModifier.In, aField.Type);
    lWrite.Virtual := VirtualMode.None;
    lWrite.Visibility := aField.Visibility;

    lWrite.ReplaceMethodBody(
        new BeginStatement(
            [new StandaloneStatement(
                new ProcValue(Methods, "Set_Field_Value", [new ParamValue(0), new ParamValue(1), StorageField], [SelfType, aField.Type])
                                    )]
                             ));

//private read method for field replacing property
    var lRead := aField.Owner.AddMethod('get_' + aField.Name, aField.Type,  aField.Static);
    lRead.AddParameter('self', ParameterModifier.In, SelfType).Prefix := '$mapped';
    lRead.Virtual := VirtualMode.None;
    lRead.Visibility := aField.Visibility;

    lRead.ReplaceMethodBody(new BeginStatement(
        new BeginStatement(
            [new StandaloneStatement(
                new ProcValue(Methods, "Set_Field_Value", [new ParamValue(0), StorageField], [SelfType, aField.Type])
                                    )]
                             ));


//the field replacing property - visibility the same as the original field (works 100%)
    var lProp := aField.Owner.AddProperty(aField.Name, aField.Type, aField.Static);
    lProp.Locked := true;
    lProp.ReadMethod := lRead;
    lProp.WriteMethod := lWrite;
    lProp.Visibility := aField.Visibility;

//remove the original field (works 100%)
    aField.Owner.RemoveField(aField);
end;

end.

As soon as the aspect is applied (changed or added code) the text resource is added to a unit and compiled for the used platform, making the static methods available for the aspect code.

Because of the compilation of the code to an object that is linked into the rest of the code, this also works for any language.

That would still make it platform specific, as the code would be .NET, but there aspect could be applied to a different platform project.

We’re looking into (longer term) ways to rework Cirrus, especially as we’re moving the compiler to be independent from requiring .NET (ie run as native Island) in the long, long term. The proper way to handle your request would be to use something similar to the metadata used for inline methods for the captured code, and then compile it for the relevant platform.

There’s a lot of different aspects (pun not intended) to this, so this is not something that’ll change super soon, but we’ll review what ee can do here to make that part easier, as well.

1 Like

Yes, That is why I said it should be saved as text, not compiled to IL.
It should only be compiled when it is added to a project when the linked aspect is executed.
Then it will be compiled on that specific platform.

Of cause there will be much restrictions; only code that can be compiled on any platform can be written in those methods (so use of Remobjects RTL is mandatory, use of specific .Net classes is not allowed).

In my example I already broke those rules :smile:; I used the WeakReference class from .Net - this would not be allowed in the implementation.

But sometimes, an aspect is only for a specific platform - by example, the ExtensionField aspect will only run on .Net because of the WeakReference dependency, so maybe it is an idea to specify on what platform(s) the aspect can run.

We do support that now, afaik.

Yeah, ideally an Aspect should be able to choose to be platform specific or not, depending on what it needs to do.

there are a few things you might find useful:

https://docs.elementscompiler.com/API/Cirrus/Classes/AspectPlatformAttribute/

you can apply that to an attribute and it will only work on that/those platforms.

https://docs.elementscompiler.com/API/Cirrus/Classes/AutoInjectIntoTargetAttribute/

This injects a method into the target class, when applied to that method on the current attribute.

Can you give a simple example of this one?

Something like:

I did some test, and the method is injected, but without body …
If it is a procedure, it injects an empty method, if it is a function, it injects exit 0 as body.

The aspect code:
namespace builditAspects;

interface

uses
System.Linq,
RemObjects.Elements.Cirrus.*;

type
    [AttributeUsage(AttributeTargets.Field)]
    ExtensionField = public class(Attribute, IFieldInterfaceDecorator)
private
public
    method HandleInterface(Services: IServices; aField: IFieldDefinition);
    begin
    end;
    [AutoInjectIntoTarget]
    class method IsInjected(a: String; b: Integer); 
    begin
          a:= a.Substring(0,b);
    end;
    [AutoInjectIntoTarget]
    class method IsInjectedToo(a: String; b: Integer): Integer; 
    begin
          if a.StartsWith("a") then exit 1 else exit b;
    end;
end;

implementation
end.

Generates (IL):

.method public hidebysig static void IsInjected (
        string a,
        int32 b
    ) cil managed 
{
    IL_0000: ret
}

.method public hidebysig static int32 IsInjectedToo (
        string a,
        int32 b
    ) cil managed 
{
    IL_0000: ldc.i4.0
    IL_0001: ret
}
}

That sounds like a bug; can you send a full testcase?

Here it is:
TestProject.zip (20.3 KB)

1 Like

Thanks, logged as bugs://83004

Ah yes. small mistake. Btw any suggestions on making cirrus easier are always appreciated.

bugs://83004 got closed with status fixed.

The only thing I can think of at this moment is not only Cirrus.

The CC is always showing a complete alphabetic list and is selecting the entry that starts with what you already typed.
But that is completely unusable when you are looking for a *Value or *Statement (or *Exception or whatever). So it should be nice if the CC would filter out the list, only showing items that contains what you typed, so you can find the unknown entries much easier.

Maybe an option - normal CC behavior unless you type * followed by something.
The * will then activate the filtering.

I downloaded the latest build, and it is not fixed in this build; same behavior as it was.

This was fixed after the last successful CI2 build. We ran into some side effects that stopped the build from succeeding, but we should be on the way to a new build now, eta in ~1h (that’s how long they take, these days :wink:

1 Like

then done now should have the fix

Sorry, still not working in the latest version.

bugs://83004 got reopened.