Next Previous Contents

5. Introduction To Annotations

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.

5.1 A Sample Annotation

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.

5.2 Dissecting an #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.

Custom File Loader

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

Matching Rules

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: Matching can also be tuned with the #prototype and #copy directives, as discussed later.

5.3 Parameter Substitutions

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.

Metadata

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:

5.4 Variable Substitutions

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.


Next Previous Contents