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-Mx 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.
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 calling convention is always AAPCS. Because of the variations between C compilers and silicon vendor conventions, there are options for saving R9 and R12. In many cases of embedded systems compilers these registers do not need to be saved.
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.
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( 15 ) 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
);
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
Before use, you have to declare the address that holds the addressof the primary ROM table used for calling ROM functions.
variable PriPointer \ set at powerup somehow
PriPointer setPriPointer \ declare to cross compiler
Now you can define a set of ROM calls, for example,
the call to the serial version of TYPE
in a BBC
micro:bit is:
PDIC( 2, 3 ) void serType( uint8_t *buff, int length );
To call this function, use the Forth form:
<caddr> <len> serType
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.
Rudimentary support for C comments in declarations is provided, but it is good enough for the vast majority of declarations.
// ...
or /* ... */
,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 */
);
'SVC(' <n> ')' <return> [ <callconv> ] <name> '(' <arglist> ')' ';'
'JTI(' <n> ')' <return> [ <callconv> ] <name> '(' <arglist> ')' ';'
'DIC(' <n1>, <n2> ')' <return> [ <callconv> ] <name> '(' <arglist> ')' ';'
'PDIC(' <n1>, <n2> ')' <return> [ <callconv> ] <name> '(' <arglist> ')' ';'
'DIR(' <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.
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
.
FPABI
is set to 0.FPABI
is set to 1.hardFPABI
: Floating point numbers are passed to
and from the system in the VFP registers.
The value FPABI
is set to 2.For now, the only combination supported 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.
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.
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.
: setFPABI ( u -- ) to FPABI ;
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.
: setFPsystem ( u -- ) to FPsystem ;
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.
1 value saveR9? \ --
Set true if R9 is to be saved by calls.
1 value saveR12? \ --
Set true if R12 is to be saved by calls.
: 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.
: 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.
: setPriPointer \ 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
: PDIC( \ "text" -- ; )
Declare an external API reference of the form:
PDIC( 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.
: +ForceTbits \ --
Force function addresses to have the T bit (bit 0) set
by the code that performs the call. This is the
default.
: -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.
: +SaveR9 \ --
Compile additional code to preserve R9 (platform register) across calls.
Some silicon vendors do not know what various compilers do,
so the safe thing is to use +SaveR9
.
: -SaveR9 \ --
Do not compile additional code to preserve R9 across calls.
: +SaveR12 \ --
Compile additional code to preserve R12 (Intra-Procedure call
scratch register) across calls.
Some silicon vendors do not know what various compilers do,
so the safe thing is to use +SaveR12
.
: -SaveR12 \ --
Do not compile additional code to preserve R12 across calls.
: +DebugExterns \ --
Turn on display of debug information.
: -DebugExterns \ --
Turn off display of debug information.
The types known by the system are all found in the vocabulary
TYPES(t)
. You can add new ones at will. Each TYPE
definition modifies one or more of the following VALUE
s. )
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(t)
definition can either set these flags
directly or can be made up of existing types.
: "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.
: 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 Forth.
: int32 \ --
A 32bit signed quantity.
: int16 \ --
A 16 bit signed quantity.
: int8 \ --
An 8 bit signed quantity.
: uint32 \ --
32bit unsigned quantity.
: uint32_t \ --
32bit unsigned quantity.
: uint16 \ --
16bit unsigned quantity.
: uint16_t \ --
16bit unsigned quantity.
: uint8 \ --
8bit unsigned quantity.
: uint8_t \ --
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.