This information is provided in good faith but the author cannot accept any liability for any loss or damage resulting from the use of this information.
Introduction
APCS (ARM Procedure Call Standard) is a mechanism by which procedures compiled from high level languages (e.g. C, Pascal) and those written in assembler can all call each other. The programmers reference manual (volume 4) gives details of APCS. (In fact under RISC-OS we use a variant of APCS known as APCS-R.)
Strictly this document does not describe APCS but describes APCS when used in a C like environment under RISC-OS.
Most APCS routines will at some point use the Shared C Library. This is so they can perform stack extension when the stack overflows and allocate space using the standard C routine malloc().
The Shared C Library needs to keep static data which is local to each program using the Shared C Library. Therefore this data must be kept with the program, not with the Shared C Library. The Shared C Library access this data using some internal information stored at the stack limit (sl) (given by r10 in APCS-R). This means when calling APCS procedures sl (r10) must contain the correct value.
C Programs
Compiled languages have no problem with this as the compiler takes care of it all. When the C program initialises it set's up the stack limit and other register using an initialisation procedure within the Shared C Library.
Assembler programs
Assembler programs may either setup with the Shared C Library themselves (fairly complex) or if linked with C programs then the C part can call the assembler. Writing APCS conformant routines in assembler means you must follow certain rules though. These rules are explained in the PRM but information relating to the passing of arguments such as floating point values is not given in the PRM. The section ‘Writing APCS routines in assembler’ later in this document attempts to describe how to do this.
BASIC programs and the basicC object
BASIC programs can not normally call APCS routines. It is possible to have some BASIC assembler which initialises with the Shared C Library but it is still not possible to directly call the APCS routines from BASIC without this assembler.
With the basicC Adhesive object though, it is possible to call APCS routines from BASIC and from BASIC assembler without you having to know how to initialise with the Shared C Library.
This allows you to use Shared C Library routines such as malloc() and free() directly from BASIC.
(The only penalty you pay is you must not alter HIMEM or END and therefore cannot use other third party memory allocation methods.)
Additionally you can call Adhesive objects which operate in an APCS environment (e.g. objects written in C). Alternatively you can call inline BASIC assembler which is APCS conformant.
BasicC
This Adhesive object (number 142) provides procedures to allow you to call APCS routines from BASIC using a CALL statement. An example of it's use is provided in the ‘Example’ directory.
You may distribute the basicC object with your own programs once you have registered with Adhesive.
basicC provides the following entry points:
0 basicC_StringTo0
1 basicC_StringFrom0
2 basicC_ToDouble
3 basicC_FromDouble
4 basicC_Init (must be used from CALL statement)
5 basicC_CCall (must be used from CALL statement)
Entry points 0-3 perform simple conversions these are documented in the file ‘142.100.Info’ - i.e. the information file supplied with the object.
basicC_Init
This routine must be called using a USR statement from BASIC. It returns the address of a branch table which gives access to routines in the Shared C Library. The entries in this branch table are described in the PRM and are also listed in the file ‘tools.ShLib’ supplied with Adhesive. The example provides a procedure to convert from the entry number given in the ‘tools.ShLib’ file to an address in the branch table.
This routine initialises with the Shared C Library. You use this call as follows:
1 Register with Adhesive and request the basicC object.
2 Drop HIMEM by at least 16K to provide some initial workspace for the Shared C Library. (e.g. HIMEM = HIMEM - 1024*16). Remember not to change HIMEM inside a procedure.
4 You are now ready to call APCS routines from BASIC. Do not alter HIMEM, END or your task's wimpslot after making this call.
The Shared C Library is told it can extend your task's wimpslot if it runs out. For example if you allocate large blocks using malloc().
basicC_CCall
Performs a C function call. This is done by parsing the passed arguments and building up a APCS conformant stack frame. This call is re-entrant so if you call some C code which then chains a BASIC program which calls some more C code....then this call won’t be the cause of your errors!
If you are not using BASIC64 then conversion between double and BASIC 5byte floating point numbers occurs.
routine% address to branch to (this must be integer)
return this is updated to the return value
a1..an arguments
Argument types allowed are:
Type Examples Description
byte ?a array%?3 8bit character, passed to C as integer use for char,int.
integer b% word% 32bit word, passed to C as integer
name%(n) use for char,int and pointers.
integer !c c!3 c%!2 32bit word, passed to C as integer
use for char,int and pointers.
integer array d%() Array of 32bit words, passed to C as pointer to array data. NB More than one subscript possible, subscripts are same order as in C.
string name$ String. Converted to NULL terminated string,
list$(3) passed to C as a pointer.
string $id String. Converted to NULL terminated string,
$(id+5) passed to C as a pointer.
real name Floating point value, passed to C as
name(4) double (IEEE standard).
The additional argument type below can be used under BASIC64.
real array name() Passed to C as pointer to double.
Return value types allowed:
integer r% ret%!4 32bit integer, this must be word aligned (e.g. ret%!2 not allowed if ret% word aligned)
Use for C routines which return void,int,char or a pointer.
real name Floating point value, converted from double
name(7) if not using BASIC64.
string name$ String. Converted from pointer to NULL
list$(7) terminated string. Use if C function returns char*. String is truncated to 255 characters if necessary. If the C function returns NULL the string is set to be an empty (0 length) string.
string $ident String. Converted from pointer to NULL
$(ident+3 terminated string. Use if C function returns char*.
If the C function returns NULL the string is set to be an empty string ie just a ASCII 13 terminator. You must ensure the space at ident is large enough to hold the string and the ASCII 13 terminator.
Note1: There is a fixed size (currently 1K) buffer for string conversion. An error is given if this is exceeded. Strings passed as arguments are only kept during the C call. They are destroyed when the C call returns.
Strings passed to C are always word aligned.
Note2: In ordinary BASIC (ie not BASIC64) basic 5byte values are converted to the double format and vice versa by truncation (no rounding). Double values which are too large/small are converted to the largest BASIC 5byte value or 0.
Note3: You may omit the return value and all arguments to call a C procedure void foo(void) for example. If you wish to pass arguments you must pass a variable for the return value. If the C routine does not return
a value (ie void) then the return value is undefined.
Warning:
When using CALL if you pass an undefined variable BASIC simply creates it. This can easily cause address exceptions and other nice errors if for example you drop the percent on a integer variable so basicC_CCall thinks it is a 5byte real not a 4 byte integer.
Writing APCS routines in assembler
This section describes how to write assembler routines which can be called from C and can call C routines. How the C (and Pascal and hopefully other compilers) pass arguments to procedures is described along with how C structures and Pascal records are organised.
C structures (and Pascal records)
Structures can contain several primitive types. These are integer, pointer, character, float/double and arrays of these types. When you define a structure the compiler does not reorder the items, the elements of the structure appear in memory in the order you define them.
Each of these types is the following size:
integer / pointer 32bits word aligned
float 32bits word aligned
double 64bits word aligned
Arrays of the above types simply consist of many of the same type one after another.
Characters are the exception. Characters which are next to each other in memory are packed together (byte alignment), the first character will have word alignment. If a single character is ‘sandwiched’ between two integers then it will occupy a whole word. Strings are simply arrays of characters. (In Pascal the packed keyword has no effect on arrays of characters.)
Arguments
Arguments to procedures are always passed through registers r0-r3 if possible and then are placed on the stack. An argument of type character is placed in a register on it's own - no packing occurs. Floating point values are passed through integer registers (the APCS standard permits the passing of floating point values in f0-f3 but this C compiler does not do this - partly due to C's variable argument lists). Floating points double values will therefore take up two registers. It is possible for a double to be split between r3 and the stack.
Arguments which are placed on the stack are placed such that the last argument is pushed on the stack first. This means the last thing on the stack before the routine is called is the 5th argument or the second half of the 4th argument if the 4th argument it a double.
Below is some example code which demonstrates how to perform stack checking and stack extension.
; example of writing assembler for use in APCS environment
; in STM/LDM instructions rx..ry indicates any combinations
; (0 or more) of x..y may be present
; x$stack_overflow and x$stack_overflow_1 are provided by
; the run time kernel
; this next block identifies this routine
; should a backtrace be required
= “procedure name”,0
ALIGN
& &FF0000xx ; xx is positive offset from this word back to start of string
; eg “abcde”,0 needs a value of &FF000008 (remember to include padding from ALIGN)
procedure
MOV ip,sp
; optional instruction to save arguments on entry
; STMDB sp!,{r0..r3 }
STMDB sp!,{r4..r9 ,fp,ip,lr,pc}
SUB fp,ip,#4
; only one of the following 3 pieces of code should be used next
; depending on amount of stack used etc
; (1) procedure will use no more than 512 bytes of stack and
; will not call an external procedure
; (internal routines can be considered as part of this one
; provided they use a fixed amount of stack)
; no code required
; guaranteed 512 bytes of space left on stack
; (2) procedure will use no more than 256 bytes of stack and
; may call an external procedure
CMP sp,sl
BLLO |x$stack_overflow|
; (3) (a) procedure may use more than 512 bytes of stack
; (and may or may not call external procedure)
; OR
; (b) procedure may use more than 256 bytes of stack and
; may call external procedure
SUB ip,sp,#amount_of_stack_space_needed_in_bytes
CMP ip,sl
BLLO |x$_stack_overflow_1|
; typical instruction to drop sp for local space
SUB sp,sp,#amount_of_stack_space_needed_in_bytes
; more code which uses local space,
; always accessed using positive offsets from sp
; eg STR r0,[sp,#positive_offset]
; main procedure body
; to access arguments which may have been pushed on stack
; also access with -ve offsets from fp
; eg LDR r0,[fp,#negative_offset]
; remember to count stack use of internal subroutines
; as part of this procedure
BL internal_subroutine
; when calling an external procedure,
; sl,sp must be valid
BL external_procedure
; on return: r0-r3,ip,lr may be corrupted; r4-r9,sl,fp are preserved
; PSR flags are preserved
; (lr corrupted implies it may contain anything, not just the return
; address setup by the BL)
; more code
; must preserve sl,fp
; may corrupt r0-r3,ip,lr
; may corrupt r4..r9 but only if they are stacked at entry/exit
; when returning sl,fp must be valid
MOV r0,#return_value
; return with value in r0
LDMDB fp,{r4..r9 ,fp,sp,pc}^
EXTRA NOTES:
In leaf procedures (ones that call no other external procedures)
various optimisations can be made to the entry/exit sequence.
eg
procedure
LDR r0,[r0,#Offset]
MOVS pc,lr
It is important that a procedure always returns the flags using lr
(usually stacked) and not the flags on entry. Consider the following
example of a tail-optimised procedure calling an external procedure:
procedureA
CMP r0,#value
MOVLO r1,#constX
MOVHS r1,#constY
B procedureB
If procedureB was to restore the flags it saw on entry
(not those in lr) then it will return to A’s caller with the