The previous chapter focused on the use of type maps to extend SLIRP and tailor its behavior. Type mappings are simple to express and comprehend, and in many cases will be all the developer needs to create reasonably capable wrappers for a given library, especially in the initial phases of a bindings project.
We now turn to a series of features, collectively referred to as
annotations, which are arguably the most powerful SLIRP has to
offer. In exchange for learning a bit of new and initially strange
syntax, annotations provide a degree of control over the code generation
process that goes well beyond what can be achieved with type mappings
alone. For instance, consider the problem of morphing a C function of
M inputs and N
outputs into a S-Lang function of M' inputs and N' outputs.
Such might arise when wrapping a function prototyped as
int print_array_f(float *arr, int len);
where the second argument indicates the number of elements in the first.
The default wrapper generated for this function would yield S-Lang usages
such as
variable arr = [1.1, 2.2, 3.3, 4.4, 5.5];
print_array_f(arr, length(arr));
However, since S-Lang arrays "know their size" the second parameter in
the function call is superfluous and can be dropped, yielding the more
natural usage
print_array_f(arr);
In our nomenclature the C version of this function has M=2 inputs
and N=1 outputs, while the S-Lang version has M'=1 input and
N'=N outputs. To achieve this transformation a module writer
might hand-craft a wrapper along the lines of
static int wrap_print_array_f(void)
{
SLang_Array_Type *arr;
int status;
if ((-1 == SLang_pop_array_of_type (&arr, SLANG_FLOAT_TYPE))
return -1;
status = print_array_f(arr->data, arr->num_elements);
SLang_free_array(arr);
return status;
}
This approach works, but is laborious and requires in-depth knowledge of
the S-Lang C api (e.g. to correctly use the SLang_Array_Type).
Furthermore, since the pattern captured is a common one it would be
better if such transformative wrappers could be generated in a more
automated fashion.
SLIRP annotations serve exactly this purpose, by enabling developers to tag function prototypes with semantic hints and source code fragments; with these additional semantics SLIRP is able to make more informed judgements about what prototypes mean; the additional code fosters the creation of custom wrappers, without resorting to writing them entirely by hand.
Some users may recognize a resemblance --- in both spirit and
syntax --- between SLIRP annotations and SWIG %typemaps.
This is not accidental, and it is only appropriate that we acknowledge
the numerous insights that have been gleaned from SWIG through study of
its %typemap capability.
By adding to the interface file an annotation such as
#argmap(in, which=1) (float *arr, int)
$2 = ($2_type) $1_dim0; /* float* argmap */
#end
SLIRP would generate a wrapper for print_array_f resembling
static void sl_print_array_f (void)
{
int result;
float* arg1;
Slirp_Ref arg1_ref = Slirp_ref_init(SLANG_FLOAT_TYPE, sizeof(float), arg1);
int arg2;
if (SLang_Num_Function_Args != 1 ||
pop_array_or_ref( &arg1_ref) == -1 )
{Slirp_usage_err("int = print_array_f(float_ptr)"); return;}
arg2 = (int) Slirp_ref_get_size(&arg1_ref,0); /* float* argmap */
result = print_array_f(arg1, arg2);
(void) Slirp_ref_finalize(&arg1_ref);
(void) SLang_push_int ( result);
}
The desired effect has been achieved: the generated code expects
only an array argument to be passed in from S-Lang scope. Fewer lines of
hand-written code (3 lines in an interface file, versus 10+ lines in C) were
needed to wrap the function, while bookkeeping work --- such as registering
wrappers within an intrinsic function table, merging manually-crafted code
fragments with automatically-generated code, and writing usage statements
--- is virtually eliminated. The fact that the annotation does not reference
the print_array_f function by name allows it to be used on any function
with a matching prototype, and can sharply reduce the amount of coding
required to create custom wrappers.
Annotations may be used for other purposes as well, such as omitting return
values, making values returned through a function parameter list in C appear
as if they were returned on the stack in S-Lang, or injecting user-defined
fragments of C code into the generated wrappers. These and other ideas are
explored in the context of numerous examples, many of which can be found in
interface files bundled within the examples directory tree of
the SLIRP distribution.
#argmap
The syntax of an #argmap may seem peculiar to the uninitiated, and
you would be justified in wondering at this point How does
it actually work? This question actually has two
parts: the first concerns how annotations are read by the S-Lang
interpreter, and is relevant because their semantics are expressed in
a syntax not defined within the S-Lang grammar; the second concerns how
SLIRP selects which annotations to apply while generating wrappers.
The first part of the question is by far the easier to answer: SLIRP contains a custom file loader, installed via a load file hook, which S-Lang will call when evaluating scripts. This loader scans the input interface file for directives (which happen to look like preprocessor tokens) marking an annotation block. Each annotation directive utilizes a unique callback function which
The second key to using annotations is understanding the pattern matching rules employed by SLIRP to decide whether a given annotation should be applied to some function. For example, given
#argmap(in, which=2) (long nelems, char **array)
$1 = ($1_type) $2_dim0; /* char** argmap #1 */
#end
#argmap (in) char **
/* char** argmap #2 : just a single comment */
#end
and a function prototyped as
extern void print_array_s (long nelems, char **array);
only the first mapping would be applied, since it is a stronger match. Now
if the mapping
#argmap(in, which=2) (long, char **)
$1 = ($1_type) $2_dim0; /* char** argmap #3 */
#end
were also present which would SLIRP select? Still the first, since not only
does it match the prototype in the quantity and type of its
parameters, but also in their names. However, if the prototype instead
looked like
extern void print_array_s (long, char **);
then the first #argmap would be rejected (because parameters named within
an annotation do not match unnamed parameters within a prototype) and the
third would be used (since it provides a longer match than the second).
Finally, if the first and third mappings were removed then the second
would be applied and yield broken runtime behavior, since the array
size parameter would remain uninitialized in the wrapper (the argmap
body contains only a comment). The rules governing the matching of
annotations with function prototypes may thus be summarized as:
#prototype and #copy directives, as
discussed later.
At this point it should be clear that parameters specified within the
parameter list of an annotation are referenced elsewhere within the
annotation by prefixing them with a dollar sign. That is,
the first parameter is referred to as $1, the second as $2, et
cetera. This allows the module writer to craft code fragments which
explicitly refer to function arguments, irrespective of the names SLIRP
later generates for their corresponding local variables.
When the body of the annotation is injected into the wrapper SLIRP
substitutes each $-delimited reference with the name of the respective
local variable. While at first glance this may seem trivial, you should
recall that because multiple annotations can be applied to a function it
is by no means clear what names SLIRP will assign to each variable
declared within its wrapper.
SLIRP also provides lightweight introspective capabilities, loosely analogous to reflection in Java, which enable annotations to discern metadata about a parameter, such as its type, size, or argument number. These substitutions are:
#argmap(in) LongInt (tmp)
...
tmp = $argnum;
...
#endif
would be applied twice to a function prototyped as
void diff(LongInt x1, LongInt x2);
causing the lines
tmp1 = 1;
...
tmp2 = 2;
to appear within its generated wrapper.
dimI
Yields the size of the I-th dimension (numbered from 1)
of argument n when it is an array, otherwise 1. When I
is greater than the number of the dimensions in the array
the size returned will be 0. When I is 0 the size
returned will be the number of elements in the entire array.
length
Equivalent to $n_dim0.
ndims
Yields the number of dimensions of argument n when it is an
array, otherwise 1.
nullify
Yields code which will set argument n to NULL. For non-opaque arguments
this is trivial (and can be done through other means), but for
opaquely typed arguments the wrapped object
will be set to NULL. This might be used in conjunction with the
final method (described below) to avoid manual coding in cases
where one wants to prevent an opaquely typed
variable from pointing to freed memory.
type
Yields the C type of the local variable corresponding to
argument n. In the above example this substitution is used to cast
the value yielded by $n_dim0, so as to avoid potential
compile warnings.
Another form of substitution is performed when an annotation includes variable declarations, as in
#argmap (in) char ** (int size)
{
char **copy = $1;
size = 0;
while (*copy++)
size++;
}
printf("\nNull terminated string array size: %d elements\n",size);
#end
This annotation declares an integer size variable, and if applied to a
function prototyped as
extern void print_array_nts (char **array);
(where the final element of array is expected to be NULL) would yield
a wrapper resembling
static void sl_print_array_nts (void)
{
int size1;
char** arg1;
...
{
char **copy = arg1;
size1 = 0;
while (*copy++)
size1++;
}
printf("\nNull terminated string array size: %d elements\n",size1);
...
}
Here the size declaration maps to the size1 automatic variable.
A numeric suffix is used to uniquely identify the instance of the declared
variable since, as noted above, a single annotation might match multiple
parameters within a prototype, causing its code fragment to be injected
into the generated
wrapper multiple times. Notice that locally-scoped variables, such
as copy, may also be declared and used within inner blocks. Unlike
wrapper-global variables, however, these do not require disambiguation.