Tom Lord's Hackery

Anatomy of a Librified libarch Function

#include <tla/libarch/arch.h>

It may be helpful to read this document in parallel with the template C code for libarch functions

The template is described in detail in the appendix Template Details.

t_arch const arch -- The Universal Function Parameter

Every ordinary function in libarch accepts a parameter of type t_arch, conventionally named arch. Some very low level functions are an exception to this rule.

By convention, the output parameters to a function are first in its parameter list, then the t_arch const arch parameter, and then any input parameters.

Arch Sessions and Arch Calls

The arch parameter holds the state, including dynamically allocated resources, of a single "arch session" and of all "arch calls" within that session.

An arch call is the invocation and execution of a single, client-facing, ordinary libarch function. Typically an arch call performs a primitive arch operation or calculation. A call may temporarily allocate resources as it runs; these resources are released before the call returns to the client. During a single call, temporary resources are "attached" to the t_arch const arch and, when a call returns, are automatically released.

An arch session is a sequence of calls which may share persistent state among themselves. A t_arch const arch value is the name for a single context in which such persistent state can be recorded. For example, the location of a user's .arch-params directory defaults to ~/ but it might as easily be an element of the persistent state of a t_arch const arch object. A client can allocate a t_arch, set its .arch-params location to a non-default location, and then perform a series of arch calls with that t_arch. Those arch calls will use the non-default .arch-params location. Other calls in the same process, using a different t_arch, might use a different .arch-params location.

Anatomy of an Arch Function

Entry and Exit

libarch extends the standard C function prolog and postlog with its own conventions. Every libarch function must be of the general form (here illustrated for a function which can not raise errors):


    int
    some_fn (t_arch const arch)
    {
      int answer = -1;

      arch_enter_call (arch);

      /* ... compute the real answer.  Set `answer'.  ... */

      arch_leave_call (arch);
      return answer;
    }


    

Unbalanced calls to arch_leave_call constitute an invariant violation resulting from a coding error and cause a call to panic.

Error Codes and Raising Errors

libarch uses dynamically assigned positiveinteger error codes when reporting error conditions to callers. Must functions which can indicate an error do so by returning a negativeinteger whose magnitude (additive inverse) is an error code.

In addition to signalling errors via return value, functions must explicitly raise errors, which records then in the t_arch const arch parameter. Typical code looks (schematically) like this:

    ssize_t
    some_fn (t_arch const arch)
    {
      ssize_t answer = -1;
      ssize_t errcode = 0;

      arch_enter_call (arch);

      /* ... compute the real value for `answer' but:
       */
      if (... an XYZZY error has been detected ...)
        {
          errcode = error_arch_XYZZY ();
          goto error_exit;
        }

      /* ... assuming that `answer' is now set 
       */
      invariant (answer >= 0);  /* normal return can't be negative --
                                 * that's reserved for errors.
                                 */
      arch_leave_call (arch);
      return answer;


     error_exit:
       invariant (errcode > 0);
       arch_RAISE (arch, errcode, "some_fn");
       answer = -errcode;
       arch_leave_call (arch);
       return answer;
    }


    

See also Error State Transition Table.

Error Messages -- The msgbuf

Between a call to arch_enter_call and a call to arch_RAISE, clients can format arbitrary text to be associated with the raised error. (The intent is that this is text which might be displayed to a user.)

The exmaple above might be modified to contain:

      if (... an XYZZY error has been detected ...)
        {
          arch_msgbuf_printfmt (arch, "an xyzzy error caused by %s\n", xyzzy_errstr());
          errcode = error_arch_XYZZY ();
          goto error_exit;
        }

    

Managing Allocted Local State

Commonly, libarch functions need to allocate temporary strings, lists, and tables to contain the results of intermediate computations. Those data structures must be reliably freed before returning to the caller. The situation is common enough that libarch includes help for it.

Local allocated values, when allocated, are "attached" to a t_arch const arch value. When the call is exited (indicated by a call to arch_leave_call), those attached resources are freed:

WARNING: this interface is not fully implemented yet and is subject to minor changes as it is finalized.

    ssize_t
    some_fn (t_arch const arch)
    {
      ARCH_FN_DECL;
      ssize_t answer = -1;
      t_local_val tmp_str = LOCAL_VAL_NIL_INIT;
      ssize_t status = -1;

      arch_enter_call (arch);


      /* ... */

      /* e.g.: allocate a local string
       */
      status = val_make_str (addr_local_val (tmp_str, arch),
                             arch,
                             "hello world");
      if (status < 0)
        {
          errcode = arch_error_ENOMEM ();
          goto error_exit;
        }

      /* ... */

      arch_leave_call (arch);
      /*
       * The call to `arch_leave_call' above will implicitly 
       * release the storage allocated for `tmp_str'.
       */

      return answer;


     error_exit:
       invariant (errcode > 0);
       arch_RAISE (arch, errcode, "some_fn");
       answer = -errcode;
       arch_leave_call (arch);
       return answer;
    }


    

Appendix: {Template Details}

The file src/tla/Template-fn contains a template for librified libarch functions. The template begins:

Function Prototype

    /** function: FN_NAME
     *
     */
    ssize_t
    FN_NAME (t_arch const arch)

    

there and elsewhere, replace FN_NAME with the name of the function you are defining.

Most functions should return a ssize_t value, a negative integer indicating an error (whose error code is the magnitude of that negative integer). Functions with no other use for the ssize_t return value should return 0 upon success. Other functions may return any non-negative value upon success.

Output parameters should be declared prior to the arch parameter; other parameters following the arch parameter. All parameters should be declared const. Normally, each parameter should be declared on a separate line. For example:

    ssize_t
    some_fn (int * const n_ret,
             t_arch const arch,
             int const x,
             int const y)

    

Local Variable

All non-const local variables must be declared in the outermost block of a function. The outermost block in the template begins:

    {
      ARCH_FN_DECLS;
      ssize_t answer = -1;
      int pc = 0;
      ssize_t errcode = 0;

    

Additional local variables may be added to that list.

The sole exception to this role is local variables which are const. Such variables may be declared (and necessarily initialized) at the top of nested blocks.

No local variable in the outermost block may have a non-constant initializer or may have an initializer which makes a call to a function (even a const function).

Function Entry

The body of the template reads, schematically:

    if (pc == 0)
      {
        /* init */
        arch_enter_call (arch);
        goto body;
      }
    else
      {
        [... error handling code ...]

      normal_exit:
        invariant (answer >= 0);
        goto exit_with_cleanups;

      exit_with_cleanups:
        goto exit_immediately;

      exit_immediately:
        arch_leave_call (arch);
        return answer;
      }

    body:

    [... main body of the function ...]

    goto normal_exit;


    

Programs may add code to the section labeled /* init */ between the call to arch_enter_call and the goto body statement. Coded added in that location should be solely for the purpose of initializing local variables to values which are not constant. For example, a local variable declared:

     time_t now = 0;

    

might be initialized in the /* init */ section with:

     time_t now = time (0);

    

It could not have been declared these ways:

     time_t now = time (0);             /* illegal (by convention) */
     time_t const now = time (0);       /* illegal (by convention) */

    

because local variable initializers at the top of a libarch function may not make function calls.

Single Entry, Single Exit

The template contains the code:

      exit_immediately:
        arch_leave_call (arch);
        return answer;

    

That is the only return statement in the template and programmers must not add addition return statements to the function definition. All exit paths from the function must pass through the code labeled exit_immediately.

The call to arch_leave_call provides run-time enforcement of an open ended set of calling convention invarients. It has has responsibility for releasing temporary resources allocated by this function call.

Error and Normal Returns but a Single Cleanup Path

The template contains:


    error_exit:
      invariant (errcode > 0);
      arch_RAISE (arch, errcode, "FN_NAME");
      answer = -errcode;
      goto exit_with_cleanups;

    efatal_exit:
      errcode = error_arch_EFATAL ();
      goto error_exit;

    eparam_exit:
      errcode = error_arch_EPARAM ();
      goto error_exit;

    normal_exit:
      invariant (answer >= 0);
      goto exit_with_cleanups;

    exit_with_cleanups:
      goto exit_immediately;


    

Programmers may delete the code labeled efatal_exit and eparam_exit if those exit paths are not needed.

All error exit paths from the function should involve setting errcode to a positive integer value and transfering to error_exit.

All ordinary exit paths from the function should involve setting the variable answer to the return value from the function and transfering to normal_exit.

When those conventions are followed, execution passes through the code labled exit_with_cleanups and finally to`exit_immediately'.

Programmers may (have to) customize the template in two ways: (1) By adding resource-releasing code to the section labled exit_with_cleanups; (2) by modifying the invariant's in error_exit and normal_exit to more precisely reflect the range of expected return values from the function.

The Main Function Body

The template finishes with:


   body:

    goto normal_exit;


    

Programmers must customize that part of the template by filling in code implementing the function between the body and the goto. In some cases, programmer's must replace the label in the goto or replace the entire goto with /* not reached */.

Appendix: {Error State Transition Table}

Any libarch function (client facing or ordinary internal function) can raise an error. Caller's must respond to all errors raised.

To an imperfect but practical extent, libarch enforces that restriction. In particular, every t_libarch is partly characterized by an error state. The error state is described by three integers, according to the state diagram below.

The gist is that if a function you call raises an error, your client must either re-raise the error before returning to its caller, or must "clear" the error state.


    NORMAL state:
          depth = D
          err_depth = -1;
          last_raise_depth = -1;

      invariants:
          0 <= D

      transitions:

         * arch_enter_call (arch)

            goto NORMAL with
              new_depth := D + 1

         * arch_leave_call (arch)

            if (depth == 0)
              FATAL ERROR
            else
              goto NORMAL with
                new_depth := D - 1

         * arch_RAISE (arch, errcode, tag)

            goto ERROR_HERE with
              new_err_depth := D
              new_last_raise_depth := D


         * arch_clear_error (arch)

            goto NORMAL with
              err_depth = -1;
              last_raise_depth = -1;


    ----

    ERROR_HERE state:

          depth = D
          err_depth = E;
          last_raise_depth = L;

      invariants:
          D >= 0
          D <= E
          D == L

      transitions:

         * arch_enter_call (arch)

            FATAL ERROR

         * arch_leave_call (arch)

            if (depth == 0)
              FATAL ERROR
            else
              goto ERROR_CALLEE with
                new_depth := D - 1

         * arch_RAISE (arch, errcode, tag)

           goto ERROR_HERE

         * arch_clear_error (arch)

            goto NORMAL with
              err_depth = -1;
              last_raise_depth = -1;

    ----

    ERROR_CALLEE state:

          depth = D
          err_depth = E;
          last_raise_depth = L;

      invariants:
          D >= 0
          D < E
          D == (L - 1)

      transitions:

         * arch_enter_call (arch)

            FATAL ERROR

          * arch_leave_call (arch)

            FATAL ERROR

          * arch_RAISE (arch, errcode, tag)

            goto ERROR_HERE with
              new_last_raise_depth = D

         * arch_clear_error (arch)

            goto NORMAL with
              err_depth = -1;
              last_raise_depth = -1;


  

Copyright

Copyright (C) 2004 Tom Lord

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

See the file COPYING for further information about the copyright and warranty status of this work.