Skip to content
paulgray edited this page Sep 14, 2010 · 4 revisions

ennotation library is a simple implementation of AOP (http://en.wikipedia.org/wiki/Aspect-oriented_programming) in Erlang.

Introduction

ennotation allows you to create and use the simple function wrappers just like a specs in your code. The modules using annotations are transformed during the compilation process (parse_transform) and might be used without changing the API of the module.

Currently only before and after annotation types are supported.

Description

The annotation is a simple macro that can be put before the function definition, e.g.:

?MY_ANNOTATION(args_to_my_annotation).
my_fun(Foo, Bar) ->
    something.

Each annotation is a user-defined function with a given spec:

-type(BeforeAnnResult :: {ok, ModifiedArgs :: list(term())} | {stop, Result :: term()}).
-type(AfterAnnResult :: ModifiedResult :: term()).
-spec(annotation/4 :: (AnnotationArg :: term(), 
                       TargetModule :: atom(),
                       TargetFunction :: atom(),
                       Arguments | Result :: list(term())) -> BeforeAnnResult | AfterAnnResult).

where:
AnnotationArg is a parameter put in the code to change the annotation behaviour
TargetModule, TargetFunction are the target function specification
Arguments is a list of arguments that are passed to the function

There are two kinds of annotations:

  • before annotations – are called before the actual function is invoked and might change the arguments that are passed to the function and also break the call flow and return the desired value to the callee. before annotation should return either {ok, NewArgs} in the case when the call chain should proceed and the Args should be replaced with NewArgs list (length(Args) == length(NewArgs)) or {stop, Result} if the call chain should be broken and the Result returned to the caller immediately.
  • after annotations – are called after evaluating the actual function. They have access to the result returned from the function and can modify it. The returning value might be any Erlang term and will be return to the caller instead of the actual function result.

User might annotate the function with more than one annotation: they will be called in order of their declaration in the code. In the case when one of the before annotations breaks the call chain, neither the actual function, nor the after annotations will be invoked.

Implementation

In order to create a new annotation, module in which annotation is going to be defined must contain:

-include_lib("ennotation/include/ennotation.hrl").

attribute defining the ?BEFORE and ?AFTER macros and enabling parse transform for this module. During the compilation there is a new header file created for the given application, named “include/MODULE_NAME_annotations.hrl” where MODULE_NAME is a name of the module where annotations were defined. The new header file will contain the annotation definitions that can be used within the code.

before annotations

before annotation is created by putting ?BEFORE macro before the annotation function definiton.

For example, following annotations are defined in before.erl file:

?BEFORE.
log_before(info, Mod, Func, Args) ->
    error_logger:info_msg("~p:~p(~p) has been called!~n", [Mod, Func, Args]),
    {ok, Args};
log_before(warning, Mod, Func, Args) ->
    error_logger:warning_msg("~p:~p(~p) has been called!~n", [Mod, Func, Args]),
    {ok, Args};
log_before(error, Mod, Func, Args) ->
    error_logger:error_msg("~p:~p(~p) has been called!~n", [Mod, Func, Args]),
    {ok, Args}.

When compiling before.erl new file, include/before_annotations.hrl will be created with new macro defined there:

?LOG_BEFORE(AnnotationArg).

We can use it now in the production code:

-module(my_mod).
-include("before_annotations.hrl").
...
?LOG_BEFORE(info).
foobar(A, B, C) ->
    foobar_body.

The actual code that is going to be executed when calling my_mod:foobar(1, 2, 3) will be:

before:log_before(info, my_mod, foobar, [1, 2, 3]),
foobar(1, 2, 3).

If we would like to break the call chain, we should define a new annotation:

?BEFORE.
breaker(NewReturn, _, _, _) ->
    {stop, NewReturn}.

Then, we can use ?BREAKER(NewValue). before our functions:

?BREAKER({some, new, value}).
my_func() ->
    erlang:error({will, never, happen}).

From now every time we call my_mod:my_func() we will get {some, new, value} as a result.

after annotation

after annotation is created by putting ?AFTER macro before the annotation function definiton.

For example, following annotations are defined in after.erl file:

?AFTER.
log_after(info, Mod, Func, Result) ->
    error_logger:info_msg("~p:~p has returned ~p!~n", [Mod, Func, Result]),
    {ok, Args};
log_after(warning, Mod, Func, Result) ->
    error_logger:warning_msg("~p:~p has returned ~p!~n", [Mod, Func, Result]),
    {ok, Args};
log_after(error, Mod, Func, Result) ->
    error_logger:error_msg("~p:~p has returned ~p!~n", [Mod, Func, Result]),
    {ok, Args}.

When compiling after.erl new file, include/after_annotations.hrl will be created with new macro definition inside:

?LOG_AFTER(AnnotationArg).

We can use it now in the production code:

-module(my_mod2).
-include("after_annotations.hrl").
...
?LOG_AFTER(error).
foobar(A, B, C) ->
    foobar_body.

The actual code that is going to be executed when calling my_mod2:foobar(1, 2, 3) will be equivalent to:

Result = foobar(1, 2, 3),
after:log_after(error, my_mod2, foobar, foobar_body).
Clone this wiki locally