Annotations - MetaData and Code Instrumentation for Erlang/OTP
This library provides additional features for annotating code with metadata, as well as support for compile time code instrumentation via a parse transform.
Examples
Annotating specific functions:
-module(db).
-export([find/2, add/2]).
-include("db.hrl").
find(DbHandle, What) ->
qlc:q([I || I <- table_source(DbHandle), I.key == What]).
-transactional(requires_new).
add(DbHandle, Item) ->
db_backend:insert(DbHandle, Item).
Note that the -transactional
attribute is not usually allowed at this scope, but
the annotations
parse transform moves the metadata around and tags the
annotation so that it maps the right function.
Adding behaviour to Annotations
An annotation can be backed by a user-defined module, in which case the module may implement certain callback functions that can be used to provide instrumentation at runtime. For example, consider this logging annotation:
-module(logging).
-annotation('function'). %% the scope of this annotation
-compile(export_all).
before_advice(#annotation{data=Lvl}, M, F, Inputs) ->
log(Lvl, "before_advice - M: ~p, F: ~p, Inputs: ~p~n", [M, F, Inputs]),
Inputs.
after_advice(#annotation{data=Lvl}, M, F, _Inputs, Result) ->
log(Lvl, "after_advice - M: ~p, F: ~p, Result: ~p~n", [M, F, Result]),
Result.
log(Lvl, Message, Args) ->
case get(loglevel) of
Lvl ->
io:format(Message, Args);
_ ->
ok
end.
Naturally we'd normally want to use a proper logging framework and pass on the log level instead, but we've kept our example simple to use/understand here.
We can now use the logging annotated in our actual code:
-module(annotated).
-export([foo/1]).
-include_lib("annotations/include/annotations.hrl").
-logging(info).
-spec(foo/1 :: (term()) -> {foo, string(), term()}).
foo(T) ->
{foo, T}.
Building with rebar based project
We'll need to export the logging
annotation config in order for the parse
transform to work, which we can do using the supplied rebar plugin:
%% rebar.config
{deps, [
{parse_trans, ".*",
{git, "https://github.com/esl/parse_trans.git"}},
{annotations, ".*",
{git, "https://github.com/hyperthunk/annotations.git"}}
]}.
{plugins, [rebar_annotations_plugin]}.
{annotations, [{registered, [logging]}]}.
{erl_first_files, ["logging.erl"]}.
Building with erlang.mk based project
An erlang.mk plugin is provided for easy integration with your project.
DEPS = annotations
DEP_PLUGINS = annotations
ANNOTATIONS = logging
Run
And now for an example session:
t4@malachi:simple $ rebar clean compile
# output snipped...
t4@malachi:simple $ erl -pa ebin/
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.8.2 (abort with ^G)
1> annotated:foo("Hello world").
{foo,"Hello world"}
2> put(loglevel, info).
undefined
3> annotated:foo("Hello world").
before_advice - M: logging, F: foo, Inputs: ["Hello world"]
after_advice - M: logging, F: foo, Result: {foo,"Hello world"}
{foo,"Hello world"}
4>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
a
t4@malachi:simple $
For an example of using around advice take a look in the examples
directory.
Generating code at build time
Annotations can also opt to export additional code at runtime. Currently there are two supported methods for doing this:
- Generate a function that calls back into your (annotation) module
- Generate a function by hand
To generate code, you should export a function codegen/3
which takes three
inputs: the annotation record, the module in which the target function resides
and the current function AST (as handled by erl_syntax). This function must
return a list of targets, which can contain one of the following two kinds
of specification:
In order to utilise (1), your codegen/3
function should return tuples which
contains {AdviceFunctionName, TargetFunctionName, TargetFuncArity, Data}
.
The AdviceFunctionName
is the name of a function which will be called by
the generated function, the target function name and arity indicate what you
wish to generate and the Data
is a literal/AST containing the input(s) you
want sent to your AdviceFunctionName
at runtime. The result of this will be
a function which calls back into your module directly.
In order to utilise (2), your codegen/3
should return tuples which contain
{Name, fun()}
where Name
is the name you wish to export and the
corresponding fun is the function you wish to generate. The idea of this API
is that you can use the codegen
module from parse_trans
to generate your
function conveniently, and have the annotation processing engine deal with the
rest.
For an example of code generation in practise, take a look at the delegate library.
%% delegate.erl
-module(delegate).
-include_lib("annotations/include/types.hrl").
-export([codegen/3, delegate_advice/4]).
codegen(A=#annotation{data=Data}, _Mod, AST) ->
io:format("Annotation Data: ~p~n",[lists:keyfind(arity, 1, Data)]),
case lists:keyfind(delegate, 1, Data) of
{delegate, Delegates} when is_list(Delegates) ->
%% NB: you need a *little* understanding of erl_syntax here
{_FN, FA} = erl_syntax_lib:analyze_function(AST),
Arity = case lists:keyfind(arity, 1, Data) of
{arity, N} when is_integer(N) -> N;
_ -> FA
end,
[ build_spec(D, Arity, A) || D <- Delegates ];
Other ->
%% TODO: clearer error handling API
io:format("Other: ~p~n", [Other]),
{error, "no delegates defined"}
end.
delegate_advice(A, M, F, Inputs) ->
Argv = make_args(A, M, F, Inputs),
erlang:apply(M, F, Argv).
%% etc....
And the corresponding usage pattern:
-module(simple_log).
-export([log/3]).
-compile({no_auto_import, [error/2]}).
-include_lib("annotations/include/annotations.hrl").
-delegate([{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']},
{arity, 2}]).
log(Level, Message, Args) ->
case erlang:get({?MODULE, loglevel}) of
Level ->
io:format(Message, Args);
_ ->
ok
end.
Which when the annotation processing has finished, looks like this:
% escript deps/parse_trans/ebin/parse_trans_pp.beam ebin/simple_log.beam
%% snip....
-annotation({annotation, delegate,
{function, {simple_log, log, 3}},
[{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']}, {arity, 2}]}).
-export([info/2]).
-export([warn/2]).
-export([error/2]).
info(V73, V45) ->
delegate:delegate_advice({annotation, delegate,
{function, {simple_log, log, 3}},
[{target, info},
{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']}, {arity, 2}]},
simple_log, log, [V73, V45]).
warn(V51, V95) ->
delegate:delegate_advice({annotation, delegate,
{function, {simple_log, log, 3}},
[{target, warn},
{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']}, {arity, 2}]},
simple_log, log, [V51, V95]).
error(V60, V32) ->
delegate:delegate_advice({annotation, delegate,
{function, {simple_log, log, 3}},
[{target, error},
{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']}, {arity, 2}]},
simple_log, log, [V60, V32]).
log(Level, Message, Args) ->
case erlang:get({simple_log, loglevel}) of
Level -> io:format(Message, Args);
_ -> ok
end.
%% snip....
License
This work is distributed under a permissive BSD-style license.
Versioning
This project will adhere to the principles of semantic versioning once a first public API is declared.
Roadmap
See the TODO.md
file for upcoming plans/changes.