OEP

173

Title

Dynamic procedure and function calls

Summary

Proposal for dynamically calling procedures and functions in occam-pi (allowing, amongst other things, full recursion).

Owner

Fred Barnes <frmb@kent.ac.uk>

Status

Proposed

Date-Proposed

2009-03-12

Keywords

language dynamics

Introduction

By default, procedure and function calls in occam-pi have their memory requirements reserved in the workspace of the calling process. This means that callers of any particular PROC or FUNCTION need to know in advance how much space is required for these; i.e. that information must be available at compile time. The slight exception is for FORKed procedure calls, where the workspace (and any vectorspace or mobilespace) is allocated dynamically at run-time. However, that information must still be available at compile-time.

The mechanisms proposed here allow this information to be provided at run-time. Amongst other things, this permits full recursion in procedure and function calls. However, unlike FORKed procedure calls, the caller is blocked until the procedure or function returns, so the total memory requirements of a system can still be determined (in the absence of recursive calling patterns). Such analysis can be done statically.

Defining dynamically callable procedures and functions

For any procedure or function to be dynamically callable, a small data stub containing the entry-point and memory requirements for it is required. This is generated through the use of the #PRAGMA DYNCALL or #PRAGMA EXPORT directives. For example:

   1   #INCLUDE "course.module"
   2 
   3   PROC foo (VAL INT v, RESULT INT r)
   4     r := v + 2
   5   :
   6 
   7   PROC bar (CHAN BYTE out!)
   8     out.string ("bar!*n", 0, out!)
   9   :
  10 
  11   #PRAGMA DYNCALL foo, out.string
  12   #PRAGMA EXPORT bar

This will generate dynamic call stubs for both the "foo" and "out.string" (from the 'course' library) procedures. Any call to a procedure or function that has been specified in a #PRAGMA DYNCALL or #PRAGMA EXPORT (regardless of where it occurs in the source file) will still be made statically. For example, the call to "out.string" in the above "bar" procedure will be made statically.

The use of #PRAGMA EXPORT is identical to #PRAGMA DYNCALL, in that is creates a dynamic call stub for the specified procedure or function names. However, #PRAGMA EXPORT additionally (and optionally) causes the creation of an export-table file, by other parts of the toolchain, containing procedure and function prototypes for #PRAGMA EXPORTed (and dynamically callable) names.

Dynamically calling procedures and functions

For procedure calls, if the compiler already knows about the procedure (i.e. it occurs in the same file, or is imported through a #USE or #INCLUDE directive), then a dynamic call can be made by prefacing the call with "DYNCALL". For example:

   1   PROC bar (CHAN BYTE out!)
   2     DYNCALL out.string ("bar!*n", 0, out!)
   3   :

The compiler assumes that the dynamic call stub will be available at link-time (i.e. in the same file or elsewhere there must be a #PRAGMA DYNCALL bar or #PRAGMA EXPORT bar). If it is not, the toolchain will fail to link the final executable. Note that it is not possible to dynamically call a function in this way.

For procedure and function calls that the compiler does not know about through normal #INCLUDE or #USE directives, i.e. those that we would probably want to call dynamically, #PRAGMA DEXTERNAL definitions must be provided. For simple cases (and trivial types), this can be done manually. For example:

   1   #PRAGMA DEXTERNAL "PROC bar (CHAN BYTE out!)"
   2 
   3   PROC test (CHAN BYTE kyb?, scr!, err!)
   4     bar (scr!)
   5   :

The compiler assumes that the dynamic call stub for the "bar" procedure is available at link-time. The contents of any particular #PRAGMA DEXTERNAL declaration should match those generated by the toolchain as a result of #PRAGMA EXPORT directives. Any mismatch between the procedure or function header here and its actual implementation results in a run-time error.

Where non-trivial types are involved, the automatically generated export-table data must be used, as it will contain type-hashes for user-defined types. For example:

  PROC args.start.end(VAL []BYTE p1,MOBILE []ARG.POS@#A32E2050 p2)

The #PRAGMA DEXTERNAL declarations must be generated manually from the export table files, but only as far as wrapping the declarations in #PRAGMA DEXTERNAL "...".

Dynamically calling procedures at run-time

An addition to the described mechanisms allows the dynamic call stub address to be determined at run-time. This is typically only useful for programs that use dynamic code loading, through whatever means, and requires that a corresponding entry-point stub address be made available somehow. A #PRAGMA DEXTERNAL directive must still be provided for these, but the name is essentially a dummy. For example:

   1   #PRAGMA DEXTERNAL "PROC blah (CHAN BYTE out!)"
   2 
   3   PROC test (CHAN BYTE kyb?, scr!, err!)
   4     INT stubaddr:
   5     SEQ
   6       ...  set 'stubaddr' to address of a compatible dynamic call stub
   7       DYNCALL blah (scr!) AT stubaddr
   8   :

Implementation notes

The mechanisms described here are tied closely with the implementation. For instance, the compiler expects dynamic call stubs to have a particular layout. Specifically, a block of 4 integers (16 bytes) that contains (in order): the entry-point address for the actual routine; the number of workspace words required; the number of vectorspace words required; and the type-hash of the interface (procedure/function header). While the compiler (and native-code translator) normally put these together from #PRAGMA DYNCALL or #PRAGMA EXPORT directives, they can be assembled manually at run-time. For example:

   1   #PRAGMA DEXTERNAL "PROC runfoo (CHAN BYTE out!)"
   2 
   3   PROC foo (CHAN BYTE out!)
   4     VAL []BYTE str IS "hello!*n":
   5     SEQ
   6       SEQ i = 0 FOR SIZE str
   7         out ! str[i]
   8   :
   9 
  10   PROC test (CHAN BYTE kyb?, scr!, err!)
  11     [4]INT stub:
  12     INT func.addr, stub.addr:
  13     SEQ
  14       -- get entry-point address of the "foo" routine
  15       ASM
  16         LD ADDRESSOF foo
  17         ST func.addr
  18 
  19       stub[0] := func.addr
  20       stub[1] := 4                        -- known for the above "foo", others will vary!
  21       stub[2] := 0                        -- no vectorspace required
  22       stub[3] := TYPEHASHOF (runfoo)      -- type-hash of the interface
  23 
  24       -- get address of the stub block itself
  25       ASM
  26         LD ADDRESSOF stub
  27         ST stub.addr
  28 
  29       DYNCALL runfoo (scr!) AT stub.addr
  30   :

Naming

Ordinarily, occam-pi procedures and functions get assigned C-compatible names, prefixed with "O_". E.g. "out.string" becomes "O_out_string" in the generated code. With dynamic calls, these are unchanged. The newly generated stubs for such routines have different names (and represent data, not a code entry-point). In the case of "out.string" this will be "DCR_out_string".

Limitations

Although this adds a good deal of flexibility when it comes to dynamic behaviour, there are currently some implementation restrictions:

OEP/173 (last edited 2009-03-12 12:46:42 by frmb)