Calling functions in other languages








Introduction

The ARM/Cortex Forth cross-compiler supports calling functions in C or any language that can provide functions that use the AAPCS calling convention. This is an ARM convention documented in IHI0042F_aapcs.pdf.

This chapter documents the Forth side of the Forth to C interface. A separate chapter discusses the C side of the interface.

In order to provide the greatest isolation between sections of code written in other languages, the functions foreign to Forth are accessed by SVC calls and/or jump tables. The simple solution just uses SVC calls for all foreign functions.

All foreign function calls are made using the AAPCS calling convention.

Calls with a variable number of parameters (varargs) are not supported.

The interface is defined for Cortex-M CPUs only. If you need an ARM32 interface, contact MPE.

This work is directly inspired by Robert Sexton's Sockpuppet interface:

  https://github.com/rbsexton/sockpuppet

His contribution and permission are gratefully acknowledged.

SVC calls

Access to functions in a library is provided by the SVC( n ) syntax which is similar to a C style function prototype, e.g.


SVC( n ) int open(
  const char * pathname, int flags, mode_t mode
);

where n is the SVC call number. Everything on the source line after the closing ')' is discarded. The example is for the function open() from a file API, and produces a Forth word open.

  open  \ pathname flags mode -- int

The parser used to separate the tokens is not ideal. If you have problems with a definition, make sure that * tokens are white-space separated. Formal parameter names, e.g. argv above are ignored. Array indicators, [] above, are also ignored when part of the names.

A small number of SVC calls are defined by the system. Regard entries 0..15 as being reserved by the Sockpuppet API.


SVC( 0 ) int GetSAPIversion( void );
\ Returns the Sockpuppet interface version.
SVC( 1 ) void * GetLinkList( void );
\ Returns the head of the linked list used to share data.
SVC( 7 ) uint32_t GetTimeMS( void );
\ Returns the value of the millisecond timer. On overflow,
this wraps around.
SVC( 14 ) void * GetDirFnTable( void );
\ Returns the address of the direct jump table, which may
\ be the same as the SVC jump table below.
SVC( 15 ) void * GetSVCFnTable( void );
\ Returns the address of the SVC jump table.

Jump tables

Calling through SVC calls is simple, but has overhead. In addition, lower priority interrupts are disabled by an SVC call and so interrupt latency suffers badly. You cannot do ticker-based timing inside an SVC-based call without messing about with the ARM/Cortex priority mechanisms. The solution is is to call the functions through a jump table as often used by an SVC call mechanism. This requires a little bit more code than the pure SVC approach, but has much better interrupt latency and gives more flexibility.

The example of open() is used again. Access to functions in a library is provided by the JTI( n ) syntax which is similar to a C style function prototype, e.g.


JTI( n ) int open(
  const char * pathname, int flags, mode_t mode
);

where n is the index (0, 1, 2, ...) into the jump table. The parsing rules are as for SVC calls. The definition results in a Forth word open.

  open  \ pathname flags mode -- int

Before use, you have to tell the cross compiler where the address of the jump table is held, and then use an SVC call find the address of the jump table.


SVC( 14 ) void * GetDirFnTable( void );
\ Define SVC call that returns the address of the
\ jump table.

variable JT     \ -- addr
\ Holds the address of the jump table.
JT holdsJumpTable
\ Tell the cross compiler where the jump table address
\ is held.

: initJTI       \ -- ; initialise jump table calls
  GetDirFnTable JT !  ;

JTI( n ) int open(
  const char * pathname, int flags, mode_t mode
);

Double indirect call tables

Before use, you have to declare the base address of the primary ROM table used for calling ROM functions. For Luminary/TI CPUs, this will probably be:

 $0100:0010 setPriTable

Now you can define a set of ROM calls, for example, again for a TI CPU.


DIC( 4, 0 ) void ROM_GPIOPinWrite(
   uint32 ui32Port, uint8 ui8Pins, uint8 ui8Val
);

where

Parameters:

Description:
Writes the corresponding bit values to the output pin(s) specified by ui8Pins. Writing to a pin configured as an input pin has no effect. The pin(s) are specified using a bit-packed byte, where each bit that is set identifies the pin to be accessed, and where bit 0 of the byte represents GPIO port pin 0, bit 1 represents GPIO port pin 1, and so on.

Returns:
None.

To call this function, use the Forth form:

  port pins val ROM_GPIOPinWrite

Direct calls

Where the address of the routine is known at the Forth compile time, you can use a direct call.

  DIR( addr ) int foo( int a, char *b, char c );

The Forth word marshalls the parameters and calls the subroutine at target address addr.




C comments in declarations

Very rudimentary support for C comments in declarations is provided, but it is good enough for the vast majority of declarations.

The example below is taken from a SQLite interface.


SVC( x ) int sqlite3_open16(
  const void * filename,  /* Database filename [UTF-16] */
  sqlite3 ** ppDb         /* OUT: SQLite db handle */
);



Format


'SVC(' <n> ')' <return> [ <callconv> ] <name> '(' <arglist> ')' ';'
'JTI(' <n> ')' <return> [ <callconv> ] <name> '(' <arglist> ')' ';'

<n>         := { literal }
<return>    := { <type> [ '*' ] |  void }
<arg>       := { <type> [ '*' ] [ <name> ] }
<args>      := { [ <arg>, ]* <arg> }
<arglist>   := { <args> | void }        Note: "void, void" etc. is illegal.
<callconv>  := { PASCAL | WINAPI | STDCALL | "PASCAL" | "C" }
<name>      := <any Forth acceptable namestring>
<type>      := ... (see below, "void" is a valid type)

The Forth <name> is case-insensitive.

As a standard Forth's string length for dictionary names is only guaranteed up to 31 characters for portable source code, long API names can cause problems.




Calling Conventions

In the discussion caller refers to the Forth system (below the application layer and callee refers to a a foreign function. The SVC() only supports the AAPCS standard as used by C.

When using calls to floating point libraries, there are three possible calling conventions, noFPAPI, softFPABI) and *\fo{hardFPABI.

For now, the only supported combination by the cross compiler uses FPsystem=2 and FPABI=2, i.e 32 bit VFP floats, a separate Forth float stack, and a hardfloat ABI.




Promotion and Demotion

The system generates code to either promote or demote non-CELL sized arguments and return results which can be either signed or unsigned. Although Forth is an un-typed language it must deal with libraries which do have typed calling conventions. In general the use of non-CELL arguments should be avoided but return results should be declared in Forth with the same size as the C or PASCAL convention documented.




Argument Reversal

The default calling convention for AAPCS is used. The right-most argument/parameter in the C-style prototype is on the top the Forth data stack. When calling an external function the parameters are reordered as required by the AAPCS; this is to enable the argument list to read left to right in Forth source as well as in the C-style operating system documentation.




Controlling external references

When using calls to floating point libraries, there are three possible values, 0 for noFPAPI, 1 for\fo{softFPABI) and 2 for hardFPABI. At present, only the hardFPABI is supported. Defines which floating point pack is installed and active, 0 for none, 1 for software floating point, 2 for VFP2 and 3 for VFP3. At present, only the VFP2 option is supported, which is binary compatible with VFP3.

: SVC(          \ "text" -- ; )
Declare an external API reference of the form:

  SVC( n ) int foo( int a, char *b, char c );

The Forth word marshalls the parameters and calls SVC/SWI n. The Forth word has the same name as the function in the library, but the Forth word name is not case-sensitive. The length of the function's name may not be longer than a Forth word name.

: +ForceTbits   \ --
Force function addresses to have the T bit (bit 0) set by the code that performs the call. This is the default. If you know that addresses in jump tables already have this bit set, you can use -ForceTbits below.

: -ForceTbits   \ --
Do not compile the code that forces function addresses to have the T bit (bit 0) set by the code that performs the call. You will have to dump a table to find this out for your system.

: holdsJumpTable        \ addr(t) --
The base address of the jump table is stored at the given target address. HoldsJumpTable must be used before any JTI( definition is made.


variable JT     \ -- addr
JT holdsJumpTable

: JumpTable             \ -- addr
Returns the target address that holds the jump table base address

: JTI(          \ "text" -- ; )
Declare an external API reference of the form:

  JTI( n ) int foo( int a, char *b, char c );

The Forth word marshalls the parameters and calls entry n (0, 1, 2, ...) in the jump table The Forth word can have the same name as the function in the library, but the Forth word name is not case-sensitive. The length of the function's name may not be longer than a Forth word name.

: DIR(          \ "text" -- ; )
Declare an external API reference of the form:

  DIR( addr ) int foo( int a, char *b, char c );

The Forth word marshalls the paramters and calls the subroutine at target address addr. The Forth word can have the same name as the function in the library, but the Forth word name is not case-sensitive. The length of the function's name may not be longer than a Forth word name.

: setPriTable   \ addr(t) --
Set the base address of the Primary ROM table used for calling ROM functions. For Luminary/TI CPUs, this will probably be:

 $0100:0010 setPriTable

: DIC(          \ "text" -- ; )
Declare an external API reference of the form:

  DIC( pri, sec ) int foo( int a, char *b, char c );

The comma betwee pri and sec is optional. This form is used when calling ROM routines in devices from TI, NXP and others. In these the primary table entries hold the address of another table. The secondary index is then used to fetch the address of the actual routine to be called. The Forth word marshalls the parameters and calls the routine. The Forth word can have the same name as the function in the library, but the Forth word name is not case-sensitive. The length of the function's name may not be longer than a Forth word name.

: +DebugExterns \ --
Turn on display of debug information.

: -DebugExterns \ --
Turn off display of debug information.




Pre-Defined parameter types

The types known by the system are all found in the vocabulary TYPES. You can add new ones at will. Each TYPE definition modifies one or more of the following VALUEs. )

argSIZE

Size in bytes of data type.

argDEFSIGN

Default sign of data type if no override is supplied.

argREQSIGN

Sign OverRide. This and the previous use 0 = unsigned and 1 = signed.

argISPOINTER

1 if type is a pointer, 0 otherwise

Each TYPES definition can either set these flags directly or can be made up of existing types.

Calling conventions

: "C"           \ --
Set Calling convention to "C" standard. Arguments are reversed, and the caller cleans up the stack. This is the default.

: "PASCAL"      \ --
Set the calling convention to the "PASCAL" standard as used by Pascal compilers. Arguments are not reversed, and the called routine cleans up the stack.

: L>R           \ --
By default, arguments are assumed to be on the Forth stack with the top item matching the rightmost argument in the declaration so that the Forth parameter order matches that in the C-style declaration. L>R confirms this and is the default.

: R>L           \ --
By default, arguments are assumed to be on the Forth stack with the top item matching the rightmost argument in the declaration so that the Forth parameter order matches that in the C-style declaration. R>L reverses this.

Basic Types

: unsigned      \ --
Request current parameter as being unsigned.

: signed        \ --
Request current parameter as being signed.

: int           \ --
Declare parameter as integer. This is a signed 32 bit quantity unless preceeded by unsigned.

: char          \ --
Declare parameter as character. This is a signed 8 bit quantity unless preceeded by unsigned.

: void          \ --
Declare parameter as void. A VOID parameter has no size. It is used to declare an empty parameter list, a null return type or is combined with * to indicate a generic pointer.

: *             \ --
Mark current parameter as a pointer.

: **            \ --
Mark current parameter as a pointer.

: ***           \ --
Mark current parameter as a pointer.

: const ;       \ --
Marks next item as constant in C terminology. Ignored by VFX Forth.

: int32         \ --
A 32bit signed quantity.

: int16         \ --
A 16 bit signed quantity.

: int8          \ --
An 8 bit signed quantity.

: uint32        \ --
32bit unsigned quantity.

: uint16        \ --
16bit unsigned quantity.

: uint8         \ --
8bit unsigned quantity.

: LongLong      \ --
A 64 bit signed or unsigned integer. At run-time, the argument is taken from the Forth data stack as a normal Forth double with the top item on the top of the data stack.

: LONG          int  ;
A 32 bit signed quantity.

: SHORT         \ --
For most compilers a short is a 16 bit signed item, unless preceded by unsigned.

: BYTE          \ --
An 8 bit unsigned quantity.

: float         \ --
32 bit float.

: double        \ --
64 bit float.

: bool1         \ --
One byte boolean.

: bool4         \ --
Four byte boolean.

: ...           \ --
The parameter list is of unknown size. This is an indicator for a C varargs call. Run-time support for this varies between CPUs and operating system implementations of VFX Forth. Test, test, test.