Capturing Model Semantics with Dynamic Aspects

A couple of week ago I found an article about the Liskov Substitution Principle and the unavoidable reference to the circle/ellipse or square/rectangle problem.

I looked to the different solutions in the wikipedia page and I though than that was actually one of the "semantic problems" computers does not face very well. So I spend sometime thinking about this and my conclusion was that the solution will depend on the intended use of the classes in a given application or model.

Then I'd got the idea of using AOP and dynamic inheritance to implement the semantics for a given application as an orthogonal feature, keeping the class hierarchy as isolated as possible. In other words, to extend a given model with aspects that encapsulates the semantics behind it.

So, I tried to implement this idea using GNU/EDMA

Introduction

Here I should provide a sophisticated introduction to the problem and the proposed solution... well, I'm still working on that but I am not having enough time for properly define this, so I will just show how it would work.

Implementing the test classes

For this quick test I used the GNU/EDMA API for dynamic class definition so I do not need to produce an interface file and dynamic library. This approach also allows me to avoid class registration in a system repository.

Let's start with the ellipse class. It looks like this:

#include <edma.h>
#include "ellipse.h"


int
ellipse_stretch_x (OBJID id, double f)
{
  ELLIPSE_data d = (ELLIPSE_data) edma_get_data_ref (id);
  d->x = d->x * f;
  return 0;
}

int
ellipse_print (OBJID id)
{
  ELLIPSE_data d = (ELLIPSE_data) edma_get_data_ref (id);

  edma_printf ("I'm an ellipse (%lf, %lf)\n", d->x, d->y);
}

int
ellipse_class_factory ()
{
  CLASSID    cid;

  // Create a SIU Proxy for semantic behavior
  cid = edma_idf_get_free_class_id (EDMA_LOCAL_CLASS);
  
  edma_idf_set_class_name (cid, "ELLIPSE");
  edma_idf_set_class_namespace (cid, "LOCAL");
  edma_idf_set_class_version (cid, 0, 0);

  edma_add_local_class_property (cid, "x", DT_EREAL64, E_L, 0);
  edma_add_local_class_property (cid, "y", DT_EREAL64, E_L, 0);
  edma_add_local_class_method (cid, "stretchX", "R64", 
			       (PPROC) ellipse_stretch_x, 0, 0, 0);
  edma_add_local_class_method (cid, "print", "", 
			       (PPROC) ellipse_print, 0, 0, 0);

  /* Mark class as a SIU Proxy */
  edma_idf_set_class_attribs (cid, 0, 0, 0);

  edma_idf_set_class_id (cid);
}
The ellipse class has two properties that I named x and y. These are the major and minor axis lengths. For this example I only defined two methods, one to stretch the X size of the ellipse and another to print some information about the object itself. The .h file just defines the data structure for direct access to the objects properties using the edma_get_data_ref function. Now we can continue with the circle class. The circle class is defined as an ellipse subclass, but overriding the print method. This is mainly a convenience modification for the tests we will execute in a while. Note that we are printing both x and y, that should have the same value for a circle, but that will make easier checking the proper behavior of our implementation. This is the circle class:
#include <edma.h>
#include "ellipse.h"

int
circle_print (OBJID id)
{
  ELLIPSE_data d = (ELLIPSE_data) edma_get_data_ref (edma_upcast_obj (id, "SUPER"));

  edma_printf ("I'm an circle (%lf, %lf)\n", d->x, d->y);
}

int
circle_class_factory ()
{
  CLASSID    cid;

  // Create a SIU Proxy for semantic behavior
  cid = edma_idf_get_free_class_id (EDMA_LOCAL_CLASS);
  
  edma_idf_set_class_name (cid, "CIRCLE");
  edma_idf_set_class_namespace (cid, "LOCAL");
  edma_idf_set_class_version (cid, 0, 0);

  edma_add_local_class_method (cid, "print", "", 
			       (PPROC) circle_print, 0, 0, 0);

  /* Mark class as a SIU Proxy */
  edma_add_local_class_superclass_by_name (cid, "ELLIPSE", "SUPER", "INNER");
  edma_idf_set_class_attribs (cid, 0, 0, 0);

  edma_idf_set_class_id (cid);
}
The file circle .h only provides the prototype for the class factory method above.
#ifndef CIRCLE_H
#define CIRCLE_H
int circle_class_factory ();
#endif

Testing our new classes

Before going on we should check that our new classes work as expected. For that purpose we write this simple test program:
#include <edma.h>

#include "ellipse.h"
#include "circle.h"

int
main (int argc, char *argv[])
{
  CLASSID   cid;
  OBJID     id;

  EDMAInit ();

  ellipse_class_factory ();
  circle_class_factory ();

  id = edma_new_obj ("CIRCLE");
  edma_wprop3 (id, "x", 11.0);
  edma_wprop3 (id, "y", 11.0);


  edma_obj_report (id);
  edma_met3 (id, "print");

  edma_free_obj (id);

  id = edma_new_obj ("ELLIPSE");
  edma_wprop3 (id, "x", 11.0);
  edma_wprop3 (id, "y", 11.0);

  edma_obj_report (id);
  edma_met3 (id, "print");

  edma_free_obj (id);

  
  EDMAEnd ();
}
Let's compile it. The line below will do the magic: gcc -o test test.c circle.c ellipse.c `edma-config --cflags-exe` `edma-config --libs-exe` And this is what the test program should show in the console:
edma@eve:~/edma_tests/LSP$ ./test 
Trying to connect to EDMA Shared Allocator Agent ... CONNECTED
++ Using Sally Shared Allocator

GNU/EDMA 0.18.2 build [Jan 22 2011][Stable][0.18.2-3] starting up...
EDMA Startup process... OK
==OBJECT REPORT==============================================
Object (0) of class (CIRCLE)
Top Level Object. Object haven't father
No PseudoFather information
Object has 1 superobjects
    Id : (SUPER) Class : (ELLIPSE) Class attached Object: (ELLIPSE)
Object has 0 subobjects
Virtual Methods in object
==END OBJECT REPORT==========================================
I'm an circle (11.000000, 11.000000)

==OBJECT REPORT==============================================
Object (0) of class (ELLIPSE)
Top Level Object. Object haven't father
No PseudoFather information
Object has 0 superobjects
Object has 0 subobjects
Virtual Methods in object
==END OBJECT REPORT==========================================
I'm an ellipse (11.000000, 11.000000)

-------------------[EDMAEnd]--------------------------------
No more process in GNU EDMA. Freeing All Resources...
Cleanup done!!
The application outputs an object report for each one of the two objects we create and then it runs the print method, providing a brief summary of the object itself (the class and the property values). The object report for the first object shows it is an object of class circle and it is linked to a superobject of class ellipse. That was how we defined it. So far so good. Time for DAOP

Creating a Dynamic Aspect with GNU/EDMA

GNU/EDMA provides basic AOP support throughout the so-called SIU proxies. This is a GNU/EDMA extension developed to integrate external systems within the GNU/EDMA framework. Basically they are GNU/EDMA classes that intercepts the GNU/EDMA primitives and then forward those calls to the real object. Normally they get the primitive parameters and transform it into a call to an external system: a Python object, a remote object, any other thing. However, this feature can be used to implement a basic AOP system. The nice thing about SIU proxies is that they can be attached or removed to objects dynamically. Let's start defining a generic SIU proxy in our applications using the run-time class definition API we used before for our CIRCLE and ELLIPSE classes.

  CLASSID   cid;
  OBJID     id;


  cid = edma_idf_get_free_class_id (EDMA_LOCAL_CLASS);

  edma_idf_set_class_name (cid, "CIRCLE_SEMANTICS");
  edma_idf_set_class_namespace (cid, "LOCAL");
  edma_idf_set_class_version (cid, 0, 0);

  edma_add_local_class_property (cid, "obj", DT_EOBJECT, E_L, 0);
  edma_add_local_class_method (cid, "WProp3", "", (PPROC) write_prop, 0, 0, 0);
  edma_add_local_class_method (cid, "RProp3", "", (PPROC) read_prop, 0, 0, 0);
  edma_add_local_class_method (cid, "Met3",   "", (PPROC) run_method, 0,0,0);
  edma_add_local_class_method (cid, "NewObj",   "", (PPROC) create_obj, 0,0,0);
  edma_add_local_class_method (cid, "FreeObj",   "", (PPROC) destroy_obj, 0,0,0);

  /* Mark class as a SIU Proxy */
  edma_idf_set_class_attribs (cid, 1, 0, 0);

  edma_idf_set_class_id (cid);

The two important things here are:
  • The SIU Proxy interface is composed of special method names to indicate which code needs to be executed when it received an specific GNU/EDMA primitive (Write Property, Read Property, Run Method, Create Object, Destroy Object)
  • Second is that we have to set the class attributes to tell GNU/EDMA that we are defining a SIU Proxy. That's the 1 in the call to edma_idf_set_class_attribs.
For our current case we will not need the NewObj, FreeObj and RProp3 methods so we will remove then in next sections and therefore we will not implement them. Now we need to define what we want the proxy to do for each GNU/EDMA primitive. That is our application semantics!

Our Application semantics

Finally we've got to the point. Our application semantics. Let's start with a simple one:
  • Our application will deal with circles and ellipses
  • When the stretchX method is executed against a CIRCLE it is no longer a CIRCLE. It becomes an ELLIPSE
  • When we set the x and y properties to the same value we want the object to effectively be a CIRCLE
This is a possible semantics for our application. Another one could be to do not allow the execution of stretchX on a CIRCLE object. For a complete list of possibilities check the wikipedia article. The approach we are presenting allows us to define an Aspect that defines the intended semantics of our application, and apply it orthogonally to a class hierarchy without modifying it. Let's start with our first semantic rule: "Applying stretchX to a CIRCLE, transforms it in an ELLIPSE". Our SIU Proxy (or Aspect if you prefer), will implement this semantics in the method interceptor code. This is the code for this first rule:
ESint32 EDMAPROC
run_method (OBJID id, CLASSID cid, EPChar met_name, EPVoid val)
{
  OBJID   obj, ex; 

  edma_rprop3 (id, "obj", &obj);
  edma_printf ("-->[LOGGING] Running method '%s' on object %d", met_name, obj);

  if (strcmp (met_name, "stretchX") == 0)
    {
      OBJID up;
     
      up = edma_remove_superclass (id, "SUPER", 0);

      edma_printf ("-->[LOGGING] Transforming object into ellipse (%d to %d)", 
		   id, up);

      // Remove subobject
      edma_swap_obj (id, up);

      edma_met3_pargs (id, met_name, NULL, 1, val);
      edma_free_obj (up);
    
      return 0;
    }
  
  return edma_met3_pargs (obj, met_name, NULL, 1, val);


  return 0;
}
This is a brief explanation of what this code does:
  • First the real object identifier is recovered. SIU proxies dynamically attached to objects gets the identifier of the object they represent in the "obj" property.
  • Then we check if the method to execute is "strertchX". If so:
    • Then we remove the link to the superclass attached to this object. That is, for a CIRCLE, the ELLIPSE superclass. Setting the last parameter to 0 tells to GNU/EDMA to do not destroy the objects, only the link.
    • Now we interchange the identifiers of our old CIRCLE object with the identifier of its superobject. This is actually the ELLIPSE part of the object that was already there.
    • Then we run the actual method in the new object and remove the CIRCLE object. Note that the edma_swap_obj command has interchanges the identifier

      Testing our semantics

      For testing our just created Aspect implementing our application semantics we will use the following code in our test application:
      
        id = edma_new_obj ("CIRCLE");
        edma_wprop3 (id, "x", 11.0);
        edma_wprop3 (id, "y", 11.0);
        edma_met3 (id, "print");
      
        edma_attach_proxy (id, "CIRCLE_SEMANTICS");
        edma_met3 (id, "print");
        edma_met3 (id, "stretchX", 0.5);
        edma_met3 (id, "print");
      
        edma_deattach_proxy (id);
        edma_free_obj (id);
      
      
      The test code is pretty simple. First we create a CIRCLE object and then, we initialise their properties. After that we call the print method to show the object status. Then we attach our dynamic aspect. First we run the method print again to check that the aspect (a couple of logging messages will be shown) has been applied and then we call the stretchX method that should change our CIRCLE into an ELLIPSE. The output of the test would be something like this:
      I'm an circle (11.000000, 11.000000)
      
      -->[LOGGING] Running method 'print' on object 2
      I'm an circle (11.000000, 11.000000)
      
      -->[LOGGING] Running method 'stretchX' on object 2
      -->[LOGGING] Transforming object into ellipse (0 to 1)
      -->[LOGGING] Running method 'print' on object 1
      I'm an ellipse (5.500000, 11.000000)
      
      The output above shows the initial print call displaying the data we used to initialise the object. Then we apply the SIU Proxy (or Aspect) and the second call to the print method is effectively logged. The next message block shows the execution of the method StretchX. The method is first logged by the proxy and, after identifying the method name, the transformation code is executed. The next call to the print method show that our object is now an ellipse with the proper properties values.

      Completing our semantics

      To fully complete the semantics defined above we need to implement the Wprop3 method in out SIU Proxy. This method will monitor the access to the x and y properties and transform the object back into a circle if this two values are the same. This is a possible implementation:
      
      ESint32 EDMAPROC
      write_prop (OBJID id, CLASSID cid, EPChar Prop, EPVoid val)
      {
        OBJID   obj, down;
        double  x, y;
      
        edma_rprop3 (id, "obj", &obj);
        edma_printf ("-->[LOGGING] Writting property '%s' on object %d", 
      	       Prop, obj);
      
        edma_wprop3_pargs (obj, Prop, val);
      
        edma_rprop3 (obj, "x", &x);
        edma_rprop3 (obj, "y", &y);
        if (x == y)
          {
            edma_printf ("-->[LOGGING] Transforming object into a circle");
      
            down = edma_add_subclass (id, edma_get_class_id ("CIRCLE"), 
      				"INNER", "SUPER");
      
            edma_swap_obj (id, down);
      
            return 0;
          }
        
        edma_wprop3_pargs (obj, Prop, val);
      
        return 0;
      }
      
      
      Every time a property is updated in our object, the aspect checks if the values of x and y are exactly the same. In such a case it converts the object back into a circle. The process is similar to the one showed in the previous section. A new subclass is linked to our current ELLIPSE object. Then the objects identifiers are swapped. That's it.

      Testing Our Semantics II

      The program below is full test of our semantics implementation:
      
        id = edma_new_obj ("CIRCLE");
        edma_wprop3 (id, "x", 11.0);
        edma_wprop3 (id, "y", 11.0);
        edma_met3 (id, "print");
      
        edma_attach_proxy (id, "CIRCLE_SEMANTICS");
        edma_rprop3 (id, "obj", &obj);
      
        edma_met3 (id, "print");
        edma_met3 (id, "stretchX", 0.5);
        edma_met3 (id, "print");
        edma_wprop3 (id, "x", 11.0);
        edma_met3 (id, "print");
      
      
        edma_deattach_proxy (id);
        edma_free_obj (id);
      
      
      The only modification is an update of the property x after calling stretchX method, in order to force the object to transform back into a CIRCLE. This is the output of the code above:
      
      I'm an circle (11.000000, 11.000000)
      
      -->[LOGGING] Running method 'print' on object 2
      I'm an circle (11.000000, 11.000000)
      
      -->[LOGGING] Running method 'stretchX' on object 2
      -->[LOGGING] Transforming object into ellipse (0 to 1)
      -->[LOGGING] Running method 'print' on object 1
      I'm an ellipse (5.500000, 11.000000)
      
      -->[LOGGING] Writting property 'x' on object 1
      -->[LOGGING] Transforming object into a circle
      -->[LOGGING] Running method 'print' on object 2
      I'm an circle (11.000000, 11.000000)
      

      Final Words on Implementation

      Some readers have already noticed that the implementation above lacks a check of the current class, that will produce several problems. Checking the object class is very simple but this check has been avoided for easy reading of the code. Furthermore, the semantics described so far could be implemented as two aspects, one changing ellipses into circles, and other changing circles into ellipses. With this approach our aspect needs to be deattached and re-attached accordingly. To finish this implementation note, we would also like to show another semantics example. In this case, the aspect actually prevents the objects to be transformed. In other words, a circle will always be a circle and an ellipse will always stay like that. This is a possible implementation for this "constrain semantics"
      
      ESint32 EDMAPROC
      write_prop1 (OBJID id, CLASSID cid, EPChar Prop, EPVoid val)
      {
        OBJID   obj, down;
        double  x, y;
      
      
        edma_rprop3 (id, "obj", &obj);
        edma_printf ("-->[LOGGING]  "
      	       "Writting property '%s' on object %d", 
      	       Prop, obj);
      
        if (strcmp (Prop, "x") || strcmp (Prop, "y"))
          {
            edma_wprop3_pargs (obj, "x", val);
            edma_wprop3_pargs (obj, "y", val);
          }  
        else
          edma_wprop3_pargs (obj, Prop, val);
      
        return 0;
      }
      
      ESint32 EDMAPROC
      run_method1 (OBJID id, CLASSID cid, EPChar met_name, EPVoid val)
      {
        OBJID    obj;
        CLASSID  cid2, cid1;
      
        edma_rprop3 (id, "obj", &obj);
        edma_printf ("-->[LOGGING]  "
      	       "Running method '%s' on object %d", met_name, obj);
      
        if (strcmp (met_name, "stretchX") == 0)
          {
            
            cid2 = edma_get_obj_class_id (obj);
            cid1 = edma_get_class_id ("CIRCLE");
            if (cid2 == cid1)
      	{
      	  edma_printf ("-->[LOGGING]  "
      		       "Cannot run StretchX on circle object");
      	  return 0;
      	}
          }
        return edma_met3_pargs (obj, met_name, NULL, 1, val);
      
      }
      
      
      
      In this case the aspect will only be applied to circle objects (ellipses are always ellipses and do not have any restriction). Whenever a property is updated, both x and y are updated with the same value, and whenever the StretchX method is invoked, it only gets actually executed for non circle objects. Just another example. The interesting thing about this, is that the same class hierarchy can be adapted to different semantics in a very easy way. The output of our test program, with an slight and obvious modification would be this:
      
      I'm an circle (11.000000, 11.000000)
      
      -->[LOGGING]  Running method 'print' on object 2
      I'm an circle (11.000000, 11.000000)
      
      -->[LOGGING]  Running method 'stretchX' on object 2
      -->[LOGGING]  Cannot run StretchX on circle object
      -->[LOGGING]  Running method 'print' on object 2
      I'm an circle (11.000000, 11.000000)
      
      -->[LOGGING]  Writting property 'x' on object 2
      -->[LOGGING]  Running method 'print' on object 2
      I'm an circle (10.000000, 10.000000)
      
      -->[LOGGING]  Writting property 'x' on object 2
      -->[LOGGING]  Running method 'print' on object 2
      I'm an circle (11.000000, 11.000000)
      
      

      Conclusions

      We have presented an approach to deal with the semantic contents of computer applications based on AOP and dynamic OO systems. This approach allows the design of the class hierarchies following the strict relationships between classes according to general rules, and later on fine tune them for every specific situation. Using AOP, different Aspects can be implemented to actually provide the specific semantics needed by a given application, in an orthogonal way, that is, independently of the actual class hierarchy, which then can be used on different context using the appropriated aspects. The circle-ellipse example has been used to illustrate this approach