AspeCt-oriented C Tutorial
Michael Gong, Vinod Muthusamy, and Hans-Arno Jacobsen
University of Toronto, September 2006 (with changes after initial publication).
Abstract
This tutorial aims at introducing the key concepts of AspeCt-oriented C. Many
examples draw from the context of operating systems. The tutorial
targets undergraduate students taking an introductory-level operating
systems course. However, all examples are of general use and the
transfer to other contexts is straight forward.
Note, for
acc V 0.5 and higher the default file suffix is
.acc. In ECE344 (Jan-April, 2007) we are using a previous version
of the compiler, which still accepts
.ac suffixed files as
default. The below tutorial assumes
.ac as default.
Aspect-oriented Programming and AspeCt-oriented C
[
back]
Aspect-oriented programming (AOP) is a programming paradigm that
allows the developer to modularise crosscutting concerns. A concern
represents a unit of functionality present in a system such as buffer
management, file access, or process management. Crosscutting
concerns, are concerns that, for a given decomposition of a system,
cannot be cleanly modularised in the system. That is they crosscut the
system implementation. Common examples of concerns that often crosscut
a system are system call tracing, integrity constraint checking, pre
and post condition checking, synchronization, accounting, and
security.
Aspect-orientation complements existing programming paradigms, such as
the object-oriented programming or the imperative style of programming
(a.k.a. the procedural paradigm of which the language C is a popular
example.)
Typical examples of aspects are logging, debugging, error handling,
security policy checking and enforcement, transaction support, and
synchronisation. Note, however, that it will depend on a specific
system implementation whether any of these concerns are indeed
crosscutting and can therefore be classified genuinely as aspects. It
does not make sense to say "
X is an aspect," without referring
to the context, that is, the system,
X is part of.
Traditional programming paradigms, such as the object-oriented or the
procedural programming, cannot cleanly modularise crosscutting
concerns. AOP aims to complement these paradigms to help increase the
modularity of a system by offering programming language-level support
to isolate and represent aspects in a program. This isolation is
crucial, as it allows the developer to maintain, test, and evolve the
aspect without having to go to all the locations in the source code
where the aspect is present in performing maintenance tasks.
AspeCt-oriented C is a research project
applying AOP concepts to the C programming language to enable
aspect-orientation for C-based systems.
AspeCt -oriented offers an
aspect-oriented extension to C and a compiler implementing the
language extension.
Many developer resources for aspect-oriented development with Java can
be found on the [[http://www.aosd.net Aspect-oriented Software
Developer]] (AOSD) community portal. A production-strength
implementation of aspect-oriented programming for Java is the
AspectJ
compiler.
As reference, the original designers of AOP described and defined the
paradigm as follows:
"An aspect is a concern that cross-cuts the primary modularisation of
a software system."([AOP])
"Aspect-oriented programming" (AOP) is invented to modularise aspects
by extending existing programming languages with constructs for
facilitating programming aspects in a modular fashion.
"Such constructs can localise the implementation of cross-cutting
concerns (aspects) in a small number of special program modules,
rather than spreading ... throughout the primary program modules."([AOP]).
General Structure of AspeCt-oriented C Programs
[
back]
A program developed with AspeCt-oriented C consists of two parts, commonly
referred to as the
core or
base program and the
aspect program.
The core program is written in plain C and the aspect program is
written in AspeCt-oriented C and C.
To help distinguish between core and aspect program AspeCt-oriented C expects
the suffix ".mc" for the core program and the suffix ".ac" for the
aspect program.
The AspeCt-oriented C compiler is designed as source-to-source
translation, reading AspeCt-oriented C and C as input and producing C
as output. AspeCt-oriented C expects ".mc" and ".ac" files and
generates ANSI-C compliant C files. The resulting C files can be
compiled by any C compiler, like gcc, cc, or xlc to generated the
object files and executable.
The AspeCt-oriented C "Hello World" Program
[
back]
The "Hello World" program in AspeCt-oriented C is based on the following core
program (hello.mc):
int main() {
printf("Hello ");
}
and the aspect program (world.ac) is:
after(): execution(int main()) {
printf(" World from !AspeCt-oriented C ! \n");
}
After compilation and running the executable, the output is:
Hello World from !AspeCt-oriented C !
Explanation:
The core program prints the first part of the message. The aspect
program defines an
advice, which
specifies:
- when to execute: after the execution of the function,
whose prototype is "int main()" and
- what to execute: print out the second part of the message.
The AspeCt-oriented C compiler processes the advice declaration from
the aspect file and the core program from the core file and generates
C sources that contain information from both files. This step is
referred to as
aspect compilation. That is the advice
specified in the aspect file is woven into the core to result in a
program that reflects both programs' intends.
A Reusable Aspect for Memory Allocation Checking
[
back]
Problem
It is common practice to check the return value after memory
allocation to ensure the return value is non null. This code often
looks as follows:
...
int *x ;
x = (int *)malloc(sizeof(int) * 4); <--- dynamic memory allocation
if (x == NULL) {
/* rountine to handle the case when memory allocation failed */
}
/* routine for handling the normal case */
...
This check should be conducted after each call to
malloc().
Since
malloc() is a widely used means for dynamic memory
allocation, the above code fragment is almost identically scatter
across the whole system. Furthermore, similar checks apply for other
memory allocation calls, such as
calloc() and
realloc(). This is an example of an aspect.
The implementation of this memory allocation checking concern is
scattered throughout the entire program. While this is a very
important check that should most definitely be performed, the code
unnecessarily distracts from the principal program logic. This makes
it difficult to focus on the essence of the program and understand the
logic. Moreover, updating the involved checking concern code would
mean that many lines of code scattered throughout the code base may
have to change. Such pervasive changes are certainly possible, but are
tedious to perform manually. Conventional decomposition techniques
(e.g., object-orientation and imperative programming) can not
completely isolate crosscutting concern from the system.
Solution with AOP
The memory allocation checking concern is a typical aspect. Its
functionality is complementary to the core program logic and its use
crosscuts the whole system. To better modularise the system, improve
maintainability, and increase code readability, the system designers
may decide to isolate the concern. This decision has to be carefully
reflected. In the above case, for example, the check is absolutely
necessary. For illustration purposes, we show how to represent the
memory allocation checking concern with AspeCt-oriented C. The checking logic
would be refactored into an aspect file, as follows:
after(void * s) : (call($ malloc(...)) || call($ calloc(...)) || call($ realloc(...)))
&& result(s) {
char * result = (char *)(s);
if (result == NULL) {
/* routine to handle the case when memory allocation fails */
}
}
Now, the core program looks as follows:
...
int *x ;
x = (int *)malloc(sizeof(int) * 4); <--- dynamic memory allocation
/* routine for handling the normal case */
...
Explanation
The advice incorporates the following two pieces of information:
- when to execute: after each call to functions whose
name is either "malloc", "calloc", or "realloc" and whose return type
is "void *".
- what to execute: the check of the value for "s". If it is null,
the failure case is invoked. The value of "s" is checked through the
context exposure feature in AspeCt-oriented C, which passes the return value of
a function call to "s").
Benefits
- The aspect program is reusable. It can be applied to any C program
using the above memory allocation and checking mechanism.
- The system is easier to understand. The core program and the
memory allocation checking concern are not entangled anymore.
- The entire memory allocation checking concern is modularised in
one file. This gives programmers a focused view of how it works.
- The system is more flexible. The memory allocation checking
concern can be left out by simply not including it in the aspect compilation
stage. Note, in this particular case, it is unlikely that a designer
would opt to exclude the checking concern, but rather use the aspect
to substitute different checking, tracing, or debugging logic.
- The code is less error-prone. The code resides in one file, where
it is easier and quicker to find errors. In contrast, similar code
pieces are often copy-and-pasted, which is often a source of bugs.
- The code is robust. Newly added dynamic memory allocation
functionality is also encompassed by the checking aspect, without
requireing special precautions.
A Reusable Aspect for Memory Profiling
[
back]
Problem
Developers are often interested in the dynamic memory use of a
program. Similarly, the number of calls to
malloc(),
calloc() or
realloc() maybe of interest.
Traditionally, this problem is solved by adding macros, linking
against different memory allocation libraries, or instrumenting the
code. While these are all viable solutions, each comes with its on set
of advantages and disadvantages.
A solution that unobtrusively adds profiling calls and removes or
disables them again when the code goes into production is a desirable
goal.
Solution with AOP
The inherent difficulty in properly modularising memory profiling is
the fact that it crosscuts the whole system. Memory profiling is a
classical example of an aspect in this situation. The following
implementation of memory profiling as aspect can be woven into an
existing program:
#include <stdlib.h>
size_t totalMemoryAllocated; <-- use global variables to account for profiling information
int totalAllocationFuncCalled;
int totalFreeFuncCalled;
void initProfiler() { <-- AspectC file can contain regular C functions
totalMemoryAllocated = 0;
totalAllocationFuncCalled = 0;
totalFreeFuncCalled = 0;
}
void printProfiler() {
printf("total memory allocated = %d bytes\n", totalMemoryAllocated );
printf("total memory allocation function called = %d \n", totalAllocationFuncCalled);
printf("total memory free function called = %d\n", totalFreeFuncCalled);
}
before(): execution(int main()) { <-- advice 1
initProfiler();
}
after(): execution(int main()) { <-- advice 2
printProfiler();
}
before(size_t s): call($ malloc(...)) && args(s) { <-- advice 3
totalMemoryAllocated += s;
totalAllocationFuncCalled ++;
}
before(size_t n, size_t s): call($ calloc(...)) && args(n, s) { <-- advice 4
totalMemoryAllocated += n * s;
totalAllocationFuncCalled ++;
}
before(size_t s): call($ realloc(...)) && args(void *, s) { <-- advice 5
totalMemoryAllocated += s;
totalAllocationFuncCalled ++;
}
before() : call(void free(void *)) { <-- advice 6
totalFreeFuncCalled++;
}
Explanation
- advice 1 and 2 are responsible for initialising the profiler and
to output the results. Typically, a C program starts and finishing its
life by executing the main function body. We therefore weave advice 1
and 2 into the execution of main().
- advice 3, 4, 5, and 6 are straightforward: they update the
profiling information whenever a function of interest is called. By
using the context exposure feature of AspeCt-oriented C, the information about
the amount of memory allocated is passed as advice parameters by using
args() in the pointcut declaration.
Benefits
The benefits are similar to the benefits demonstrated for the memory
allocation checking aspect above.
Note
The above memory profiling aspect is not thread-safe. It is left as an
exercise to the reader to make it thread-safe. Is thread-safety an
aspect?
Matching mechanism:
[
back]
Two matching mechansims are used: simple character matching and
wildcard character matching.
Simple character matching:
If there are no wildcard characters (i.e., wildcard characters are
"$", or "...") used in the pointcut declaration, AspeCt-oriented C uses simple
case-sensitive string comparison for matching against a function's
prototype.
For example:
1. The pointcut "call(int foo(int))" picks out any call to a function
having name "foo", accepting an "int" parameter, and returning an
"int". This matches the following function prototype:
int foo(int);
2. The pointcut "args(int, char)" picks out any call or execution of a
function having any name, accepting an "int", and a "char" as
parameters, and returning any value (including "void," no value.)
Among others, it matches the following function prototypes:
void foo(int, char);
int foo2(int, char);
char * foo3(int, char);
double x2(int, char);
Wildcard character matching:
Wildcard characters includes "$" and "...". "$" represents a single
string of arbitrary length, including the empty string.
"..." represents a single list of any length, including the empty
list. Using wildcards enhances the matching capabilities of AspeCt-oriented C.
For example:
1. The pointcut "call(i$t f$oo(in$))" picks out any call to a function
having a name starting with "f" and ending with "oo", accepting a
parameter of type starting with "in", and returing a type starting
with "i" and ending with "t". Among others, it matches the following
function prototypes:
it foo(in);
int foo(int);
int f1oo(in);
int f2oo(inxy);
2. The pointcut "args(int, ..., char))" picks out any call or
execution of a function having any name, accepting a parameter list
starting with "int" and ending with "char", and returning any value
(including void.) Among others, it matches the following function
prototypes:
void foo(int, char);
void foo1(int, char);
int foo2(int, char *, char);
char * foo3(int, struct A * , char);
double x2(int, int, int, char , double, char);
Pointcut Matching
[
back]
For advice code to apply at a join point, the pointcut associated with
the advice must match the join point. AspeCt-oriented C performs the
matching of function prototype join points in the program against the
pointcut signatures specified in the aspect code. Pointcut matching is
based on this information.
Example: Suppose two advice declarations are as follows:
/* advice 1 */
before() : call (void foo1()) {...}
/* advice 2 */
before() : call (void foo2()) {...}
and the core program is as follows:
/* both foo1 and foo2 are defined in other files */
/* only foo1's prototype is given here */
void foo1();
int main() {
foo1();
foo2();
}
In this example, only the foo1() call is matched by advice 1 and the
foo2() call is not matched because its prototype is unknown to the
AspeCt-oriented C compiler.
Using the AspeCt-oriented C Compiler: acc
[
back]
Command line compilation
- If there are no "include" files, use acc as follows:
> acc hello.mc world.ac <-- acc will generate hello.c and world.c
> gcc hello.c world.c
- If there are "include" files, the input file to acc must
be preprocessed. Use acc as follows:
> cp hello.mc hello_temp1.c <-- copy the original files to have ".c" suffix, because gcc does not recognize ".mc" and ".ac".
> cp world.ac world_temp1.c
> gcc -E hello_temp1.c > hello_temp2.mc <-- preprocess files, and save them to be ".mc" and ".ac" files.
> gcc -E world_temp1.c > world_temp2.ac
> acc hello_temp2.mc world_temp2.ac <-- acc will generate hello_temp2.c and world_temp2.c
> gcc hello_temp2.c world_temp2.c
Note gcc -E invokes the preprocessor only. It is like
cpp (the C PreProcessor .)
A typical makefile
A typical Makefile example involving
acc looks as follows:
a.out: hello.o world.o <-- a.out is comprised by 2 modules: hello.o and world.o
gcc hello.o world.o
hello.o: hello.mc world.ac <-- hello.o depends on hello.mc and world.ac
acc hello.mc world.ac <-- acc generates hello.c and world.c
gcc -c hello.c <-- gcc generates hello.o from hello.c
world.o: world.ac <-- world.o depends on world.ac
acc world.ac <-- acc generates world.c
gcc -c world.c <-- gcc generates world.o from world.o
Semantic checking and debugging information
[
back]
The current AspeCt-oriented C Version 0.2 has the following limitations. As
AspeCt-oriented C matures, these limitations will go away. It is therefore
recommended to practice careful coding, double checking and triple
checking your code.
- Starting with acc V 0.4 semantic checking of aspect code
is supported.
- Starting with acc V 0.5 debugging information is
generated. This serves a debugger in visualizing the original source
code when stepping through aspect code, for instance. This also serves
the ANSI-C compiler in pointing to the line number in the original
source file when reporting errors.
To aid the developer in debugging it is recommended that the generated
C sources are passed through the on Unix platforms often available
indent program, which pretty-prints the input C source file.
This helps understand the woven aspect logic and identify errors.
AspeCt-oriented C Identifiers and Keywords
[
back]
AspeCt-oriented C follows the nomenclature standards set by the emerging
Aspect-oriented Software Development
Community and uses the following terminology, identifiers and
keywords.
For a detailed language specification and the syntax of the AspeCt-oriented C
language, please refer to the latest
AspeCt-oriented C
Specification.
The following terminology is used in the presentation of AspeCt-oriented C.
- advice
- The code to execute when a join point is matched by a
pointcut defined inside the code part of a pointcut declaration.
- join point
- A well-defined point in the execution context of a program.
AspeCt-oriented C supports function-related join points.
- pointcut
- A language extension representing one or more join points.
The following keywords are defined by AspeCt-oriented C.
- after
- An advice type declarator, it represents the advice code that should
be run after the matched join point(s).
- args
- A pointcut type declarator, it represents the join points whose
argument types are matched by the pointcut specified.
- around
- An advice type declarator, it represents the advice code that should
be run and the matched join point(s), that in the absence of
proceed(), are skipped.
- before
- An advice type declarator, it represents the advice code that should
be run before the matched join point(s).
- call
- A pointcut type declarator, it represents the join points of calling
a function whose prototype is matched by the pointcut specified.
- cflow
- A pointcut type declarator, it represents all the join points
under the control flow of the specified pointcut.
- execution
- A pointcut type declarator, it represents the join points of
executing a function matched by the pointcut specified.
- infile
- A pointcut type declarator, it represents the join points which
exist in the specified file.
- infunc
- A pointcut type declarator, it represents the join points which
exist in the specified function.
- pointcut
- Associates a name to a pointcut definition. The name can then
be used in advice declarations to refer to the named pointcut
declaration.
- proceed
- Used inside an around advice function, where it identifies that the
original join point should be executed.
- result
- A pointcut type declarator, it represents the join points whose return
type is matched by the pointcut specified.
- this
- Is used inside an advice functions. It is a pointer to a struct and
allows advice code to access context information of the
matched join points. Through it, advice code can access the
function name via "this->funcName" and the join point type via
"this->kind".
AspeCt-oriented C Examples
[
back]
The following are AspeCt-oriented C examples in the context of the OS/161
operating system kernel.
- Example: after
After calling the boot() function inside kmain(), the
advice prints out a message.
after(): call($ boot()) && infunc(kmain) {
kprintf("aspect: after boot call in kmain function\n");
}
- Example: args
Description: Before calling a function which takes a pointer to a struct
semaphore, the advice checks the pointer value to ensure it is not
null.
before(struct semaphore * x): call($ $(...)) && args(x) {
if(x == NULL) {
panic("aspect: call function with null pointer\n");
}
}
- Example: around
Description: the advice replaces the body of function
null_fsync() with a message.
int around(): execution(int null_fsync(struct vnode *)) {
kprintf("aspect: skip null_fsync function, do nothing.\n");
return 0;
}
- Example: before
- See example for "args".
- Example: call
- See examples for "after" or "args".
- Example: cflow
- Coming soon.
- Example: execution
- See example for "around".
- Example: infile
Description: The advice uses a message to replace the body of all
functions whose name start with "null_" and which are
defined in a file having a name starting with "device".
int around(): execution(int null_$(...)) && infile("device$.c") {
kprintf("aspect: skip all null_ functions from device.c file.\n");
return 0;
}
- Example: infunc
- See example for "after".
- Example: pointcut
Description: After the name "CallMalloc" is associated with
a call pointcut to function kmalloc(), the name can be used
wherever a pointcut can be used to refer to the definition of the
named pointcut, such as in a before or after advice declaration.
pointcut CallMalloc(): call(void * kmalloc(size_t));
before(): CallMalloc() {
kprintf("aspect: before call kmalloc\n");
}
after() : CallMalloc() {
kprintf("aspect: after call kmalloc\n");
}
- Example: proceed
Description: kmalloc() calls are replaced by around
advice. However, the original kmalloc() is still called
inside the around advice. If the memory allocation returns a null
pointer, the kernel exits with an error message.
void * around(): call(void * kmalloc(...)) {
char * result;
result = (char *)proceed();
if(result == NULL) {
panic("aspect: out of memory\n");
}
}
- Example: result
Description: After each call to kmalloc(), the advice checks
the returned value. If it is a null pointer, the system exits with an
error message. The example has the same effect as the
proceed() example above.
after(void * s): call($ kmalloc(...)) && result(s) {
char * result = (char *)s;
if(result == NULL) {
panic("aspect: out of memory\n");
}
}
- Example: this
Description: After a call to kmalloc(), the advice checks the
returned value. If it is a null pointer, the system exits with an
error message. Furthermore, the advice prints out in which function
the failed kmalloc() occured.
after(void * s): call($ kmalloc(...)) && result(s) {
char * result = (char *)s;
if(result == NULL) {
panic("aspect: out of memory, when calling kmalloc in function %s\n", this->funcName);
}
}
The AspeCt-oriented C Compiler
[
back]
Compilation process
The AspeCt-oriented C compiler is a source-to-source translator. As input it
processes AspeCt-oriented C and C source files and produces ANSI-C compliant
files as output. The output files can be complied by an ANSI-C
compliant compiler, such as gcc.
Generated files
The following table describes the files and the by the AspeCt-oriented C
compiler expected file suffixes.
| Description | input file | generated file |
| core program | .mc e.g., hello.mc | .c e.g., hello.c |
| aspect program | .ac e.g., world.ac | .c e.g., world.c |
Debugging AspeCt-oriented C programs
AspeCt-oriented C program can be debugged just like regular C
programs, because the AspeCt-oriented C compiler generates regular C
files.
To aid the developer in debugging it is recommended that the generated
C sources are passed through the on Unix platforms often available
indent program, which pretty-prints the input C source file.
Starting with AspeCt-oriented C V 0.5 the line number and file
information from the generated files and the source files are kept in
sync.
For versions prior to
acc V 0.5 the mapping of source to
generated files is not correct. When debugging the program, the
developer has to step through the generated C source files. Below we
show such a debugging session to illustrate how debugging may still be
done.
Using the aspect-oriented "Hello World" program as an example, we
illustrate a debugging sessions below.
>acc hello.mc world.ac <-- compile by acc
>gcc -g hello.c world.c <-- compile by gcc and create debuggable executable
>gdb a.out <-- launch GDB debugger
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
(gdb) break main <-- set a breakpoint
Breakpoint 1 at 0x8048338: file hello.c, line 10.
(gdb) run <-- run a.out to the breakpoint
Starting program: /home/mwgong/temp/AspectC/ACC/src/working/example/a.out
Breakpoint 1, main () at hello.c:10
10 printf("Hello ");
(gdb) list <-- show source, it is the generated file, not the original hello.mc
5
6 int main() {int retValue_acc;
7
8
9
10 printf("Hello ");
11 {
12 world$1(); <-- inserted by acc
13 }
14 return retValue_acc;
(gdb) next
12 world$1();
(gdb) step <-- step into the "world$1" function call
world$1 () at world.c:6 <-- inside world.c, which is generated from world.ac
6 printf(" World from AspectC ! \n"); }
(gdb) next <-- back to hello.c
Hello World from AspectC !
main () at hello.c:14
14 return retValue_acc; <-- this "return" is inserted by acc
(gdb) next
15 }
(gdb) next
0x42015704 in __libc_start_main () from /lib/tls/libc.so.6
(gdb) continue <-- continue the program to the end
Continuing.
Program exited with code 040.
(gdb)