Writing module code |
The major benefit of modules is that they can link into parts of the operating system to provide new SWI calls, handle ServiceCalls, deal with exceptions, or simply sit on an event waiting for something to happen. For example, on my current setup:
*Modules
or *ROMModules
. If you
try *Help Commands
then you will see it listed first.
A quick perusal of the Arthur 1.20 module gives us the following error messages:
RISC OS 3.7 offers messages such as:
RISC OS 4 apparently includes references to chocolate. :-)
This isn't listed as a module as it isn't a module, really. It has no start code, initialisation code, finalisation code, service call handler... It doesn't need any, it is the base level of RISC OS.
Upon this, all the other modules are loaded. These all come together to form the operating system known as RISC OS.
All modules have a chunk address. This is divisible by 64, and means that each module can have 64 SWIs. The WindowManager has many, the UtilityModule has none.
The chunk numbers are, typically, allocated on a request basis. As far as I am aware, you
can complete the required entries in !Allocate, and email it to RISC OS Ltd (I think the
address is allocate@riscos.com), and they will reply
yes or no.
It is entirely possible to allocate the chunks yourself on an ad-hoc basis (I tend to reuse
the &16F00 chunk for development code), but you should not complain if it clashes with
something, neither should you EVER release such a module - it could cause untold
chaos.
An effective way to handle mode issues is to use a macro. Code can therefore be compiled for 26-bit only, or 32-bit only simply by using the macros. Then, in (say) an obey file, you can choose which module version to load depending upon what OS is running. If a 32-bit OS is running, you load the 32-bit module, and so on.
The general API is pretty much the same. The main difference is that R14 on entry is just the return address, with no flags. To preserve flags, CPSR should be stacked on entry and restored on exit. Note that you are now preserving flags across the duration of your call, rather than restoring the caller's flags (which were previously passed in R14).
SWIs are no longer required to preserve N, Z, and C. They may still set/clear to return results, but 32-bit code shouldn't assume that SWIs preserve flags. Requiring SWIs to do so would impose an unacceptable delay on the SWI system. This, incidentally, re-specifies ALL of the SWIs, and it makes it impossible for non-kernel SWIs to depend on NZCV flags on entry.
Many existing APIs don't actually require flag preservation, so you can usually get away with:
MOVS PC, ...
MOV PC, ...
LDM R13!, {...}^
LDM R13!, {...}
More 32-bit issues are included for each module entry point.
Any given system will be either 26-bit or 32-bit. It won't vary from one mode to another. Thus it is suggested that if your code supports both modes internally, you need only check the current mode in the initialisation code, rather then at the start of each entry point.
A snippet of code to check if you are in a 26-bit mode or a 32-bit mode is:
TEQ PC, PCThis sets the flags so a comparison is EQ if in a 32-bit mode (because the PC is only the PC, thus it is equal), and NE if in a 26-bit mode (because one PC is purely the PC, the other is PC with flags, thus the two are not equal).
; Check processor mode TEQ PC, PC BEQ mode_32bit ; branch to 32-bit code ; else we are in 26-bit mode
*Modules
, you are
likely to see:
FileCore%ADFS FileCore%RAMFS FileCore%IDEFS FileCore%BaseThis shows us that FileCore is providing services to three different filing systems, using three different instantations of itself. With only one FileCore module loaded.
LDR R12, [R12]RISC OS works on the above assumption, and provides a few default actions for you. For example, when a module is killed RISC OS will attempt to free claimed workspace using the private word after your finalisation code has been called.
Please note that workspace allocated through OS_Module, 6
will always start at
address &xxxxxxx4. The PRM says: "This enables code written for time-critical
software (eg sound voice generators and FIQ handlers) to be aligned within the module
body". Try it. Type *Modules
and look at the workspace column. I've
programmed RISC OS for over a decade and never realised that before!
Offset | Contains offset to |
&00 | Start code |
&04 | Initialisation code |
&08 | Finalisation code |
&0C | Service call handler |
&10 | Title string |
&14 | Help string |
&18 | Help and command keyword table |
&1C | SWI chunk base number (optional) |
&20 | SWI handler code (optional) |
&24 | SWI decoding table (optional) |
&28 | SWI decoding code (optional) |
&2C | Messages file name (optional) |
&2C | Module flags (optional) |
This is an example of the header fields for FPEmulator version 4.14 (02/10/2000), as viewed in !Zap:
00000000 : 0000 : 00000000 : Start offset
|
00000004 : ¬B00 : 000002AC : Initialisation offset
|
00000008 : C00 : 0000039C : Finalisation offset
|
0000000C : àC00 : 000003E0 : Service call handler offset
|
00000010 : 8000 : 00000038 : Title string offset
|
00000014 : xu00 : 00007578 : Help string offset
|
00000018 : 0000 : 00000000 : Help and command keyword table offset
|
0000001C : DD0 : 00040480 : SWI chunk base number
|
00000020 : Ð000 : 000000D0 : SWI handler code offset
|
00000024 : 8000 : 00000038 : SWI decoding table offset
|
00000028 : 0000 : 00000000 : SWI decoding code offset
|
0000002C : 0000 : 00000000 : Messages filename offset
|
On entry: R0 = pointer to command string, including module name R12 = private word for the currently preferred instantiation On exit: Exit using OS_Exit, or by starting another application without having set up an exit handler. Processor status: IRQ is enabled on entry. FIQ is enabled. Processor is in USR mode. Re-entrancy: Entry point is not re-entrant. 32-bit issues: Unchanged; flag preservation not required, only V is examined on exitThis is the offset to code to call if the module is to be entered as the current application. An offset of zero implies the code cannot be started this way (ie, it is purely a service module, filing system, et cetera).
The field need not actually be an offset. If it cannot be interpreted as such, then calling Run will actually execute what is assumed to be an instruction. This allows applications to have a branch at this position to allow them to be run directly, ie for testing.
This is called by the *RMRun
command.
One would assume that the Run field is mostly outdated. Way back when, Acorn thought that all major pieces of software should be implemented as modules. Now, the current thinking is a standard application written in C or another suitable high level language (suitable, unfortunately, doesn't include BASIC - which is fair enough when considering DTP programs or web browsers, as the interpretation does put a speed penalty on BASIC programs, not to mention the lack of structs and other niceties).
Whenever a module is entered in this way, it becomes the preferred instantiation.
On entry: R10 = environment string (command tail) R11 = where module has come from: 0 - Loaded from filing system (ie, *RMLoad) >&30000000 - Loaded from podule (R11 is podule base address) Other values - Reincarnated, <R11> other incarnations exist R12 = Pointer to private word (if non-zero, implies reinitialisation) R13 = Supervisor stack On exit: Must preserve processor mode and interrupt state. R0 - R6, R12, R14 and flags (except V) can be corrupted. R7 - R11 and R13 must be preserved. Use R14 to return (ie, MOV PC,R14). Processor status: IRQ and FIQ are enabled. Processor is in SVC mode. Re-entrancy: Entry point is not re-entrant. 32-bit issues: Undefined, would assume flag preservation not required.This is called when module loaded, or after an RMA 'tidy'. It is defined that a module will not be called via any other entry point until after the initialisation code has been called. Therefore, you should, here, set up sufficient (ie, claim workspace) to make it safe to call any of the other entry points.
The PRM says:
"An offset of zero means the module does not need any initialisation. The operating
system does not provide any default actions."
If the module is being re-entered after an RMTidy, the private word may be non-zero. This is the contents of the private word prior to finalisation, relocated (if necessary) by the operating system.
Typically, you'd claim workspace and set the private word to point to it. Otherwise, you would link on to vectors or declare yourself if necessary (ie, a filing system).
A module can refuse to initialise. If an error is generated (set R0 to indicate the error and return with V set), the operating system will automatically remove the module from the RMA.
On entry: R10 = Fatality indicator 0 - non-fatal finalisation 1 - fatal finalisation R11 = 'dynamic' incarnation number. R12 = Private word, for this instantiation. R13 = Supervisor stack. On exit: Must preserve processor mode and interrupt state. R0 - R6, R12, R14 and flags can be corrupted. R7 - R11 and R13 must be preserved. Use R14 to return (ie, MOV PC,R14). Processor status: IRQ is not altered. FIQ is enabled. Processor is in SVC mode. Re-entrancy: Entry point is not re-entrant. 32-bit issues: Unchanged; flag preservation not required, only V is examined on exitYou should not enable interrupts if they are off unless you can cope with being entered via the service call handler at that stage.
If the call is fatal, the workspace will be freed and the workspace pointer is set to zero. If the call is non-fatal (ie, a tidy), then the workspace/pointer will be relocated - assuming they were allocated with OS_Module's Claim.
If the module generates an error, it remains in the RMA and is assumed to be initialised. The only way to remove a module in this state is via a module killer utility, or hard-reset.
If you are refusing to die, set R0 and return with V set.
Somehow, this behaviour is different to that of the above paragraph. The PRMs could have
been clearer on this.
The module is 'de-linked' during a finalisation call. You should not call your own SWIs or commands.
If there is no finalisation entry, workspace is freed (if pointer is non-zero) and the module is quit by the operating system.
R11 contains the dynamic instantitation number. It is called this because it is the position of
the instantiation in the instantiation list. However, position in the chain can vary, and the
length of the instantiation list can also change. In other words, this is pretty useless really.
By the way, I've used the word instantiation five times in this paragraph!
On entry: R1 = Service number. R12 = Private word, for this instantiation. R13 = Supervisor stack. On exit: R1 can be zero to claim service. R0, R2-R8 may be used to pass back a result, depending upon service call. Registers must not be corrupted unless they are returning values. R12 may be corrupted. Other constraints depend upon the service. Use R14 to return. Processor status: IRQ is not altered. FIQ is enabled. Processor is in SVC or IRQ mode (depending on service). Re-entrancy: Entry point may be re-entered by RISC OS. You must be able to handle this. 32-bit issues: Unchanged; flags ignored on exitThis allows service calls to be recognised and dealt with. An offset of zero implies that the module is not interested in service calls.
You can:
It is important that you reject unwanted service calls as quickly as possible. The PRM gives the following example (which, note, is NOT 32-bit compliant, but can be made so fairly easily):
.service_call_handler TEQ R1, #Service_<1> TEQNE R1, #Service_<2> TEQNE R1, #Service_<3> MOVNES PC, R14 ; reject unrecognised asap STMFD R13!, { registers, R14 } LDR R12, [R12] ; if workspace pointer required TEQ R1, #Service_<3> ; find out which we've got BEQ svc_3 TEQ R1, #Service_<2> BEQ svc_2 .svc_1 code to handle service call 1 ; if not 3 or 2, must be 1 LDMFD R13!, { registers, PC }^ ; and return (not 32-bit friendly) .svc_2 code to handle service call 2 LDMFD R13!, { registers, PC }^ ; and return (not 32-bit friendly) .svc_3 code to handle service call 3 LDMFD R13!, { registers, PC }^ ; and return (not 32-bit friendly)You may be able to change the
LDMFD R13!, { ... }^
to a 32-bit friendly version.
Refer to the documentation for the service call required to see if flags should be preserved.
Some service calls indicate errors by setting registers on exit (the V set convention cannot be used). Others (like unknown OS_Byte) can either claim it (no error will be given) or ignore it (error will be given if nothing claims it). Thus, if you wish to provide things like unknown OS_Bytes and generate errors for stuff like invalid parameters, you should use the OS_Byte vector instead.
Only R0-R8 can be passed to a service call.
*Modules
command.
*Help
before any information from
the module, for example *Help Modules
or *Help <modulename>
.
<modulename>[<TAB>(if necessary)]v.vv (DD MMM YYYY)
IDEFS 1.11e (20 Oct 1999) ROM Patches 2.02 (24-Feb-97) IDEFSFiler 1.09 (07 May 1998) MyProtection 0.01 (19 Apr 1999) Extended assem 1.77 (01 Mar 1998) © Darren Salt DiscAccess 0.01 (14 Oct 2000) [http://www.heyrick.co.uk/assembler/] SerialDriver 0.41 (20 Jan 1995) for issue 2 cards (slot0) Font Manager 3.37 (05 Mar 1996) (Background blending) TaskKiller 1.06 (14 Jan 1996) © Justin Fletcher HTTP Support 1.21 (16 Nov 1999) (+128-bit SSL3) © ANT Ltd, 1995-9 HotBlanker 1.00 (19 Dec 1995) © DizzyWizard Software 1995As you have noticed, there is some other stuff following. However the inportant things are present, the module number (which should be the x.xx form, nothing else) and the date (which should be day-as-a-number (with leading zero, if necessary), month-as-three-letters, and a full four-digit year). The first two examples are breaking the rules slightly.
You can use spaces in module names. Indeed, a fair few modules do this... "MOS Utilities", "Podule Manager", "Parallel Device", "Int'l Keyboard", and so on.
It is a table consisting of a list of keywords with pointers to associated help text. In the
case of commands, entry address, parameter limits, et cetera.
The table consists of a sequence of entries, terminated by a zero byte. Each entry has the
following format:
Purpose | |
String to match, null terminated (ie, the keyword) | |
(align to a word boundary) | |
Offset of code (from module start) | |
Information word | |
Offset of invalid syntax message from module start, null terminated | |
Offset of help text from module start, null terminated |
*Help Commands
will
give you the idea.
The code offset is used for commands. If this is zero, then it means that the string only
has help text associated with it. This is shown in the DiscAccess
module.
The code is entered with:
On entry: R0 = Pointer to command tail (which you can't overwrite). R1 = Number of parameters as counted by OSCLI. R12 = Pointer to private word for this instantiation. R13 = Supervisor stack. R14 = Return address. On exit: R0 = error pointer, if anything went wrong. R7 - R11 must be preserved. Processor status: IRQ and FIQ are enabled on entry. Processor is in SVC mode. Re-entrancy: Entry point is not re-entrant. 32-bit issues: Unchanged; flag preservation not required, only V is examined on exit
R1 is set to the number of parameters as counted by OSCLI. Thus, space-seperated entities are counted as parameters unless they are within quotes. For example:
Command tail Parameters one two three four 4 one "two three" four 3 ("two three" is counted as one parameter) one,two,three,four 1 (commas, no spaces)
The information word contains limits on the number of parameters, and flags:
On entry: Byte 0 = Minimum number of parameters (0 - 255). 1 = OS_GSTrans map for first 8 parameters. 2 = Maximum number of parameters (0 - 255). 3 = FlagsByte one works as a bitmap selecting which of the first eight parameters are passed to OS_GSTrans and which are passed directly to the module. Bit zero is the first parameter, bit seven is the eighth. If the bit is set, the parameter is GSTrans'd before being passed to the module. If the bit is clear, it is passed directly to the module.
The flags are as follows:
Set Alias$ADFS Help
Help
Unset Alias$ADFS
to restore normality)Eject
won't work, but CDFS:Eject
will.Logon
, Mount
, Devices
et cetera.
Status
and Configure
. The code
should then scan the command tail and either return a status string, or alter CMOS RAM
as appropriate. The code is called with R0 set as follows:
On entry: R0 = 0 *Configure issued with no option. Print a syntax string and return. R0 = 1 Status option has been used. Print the current configuration status for the option.If R0 is neither zero nor one, then the option has been called by
Configure
,
R0 is therefore a pointer to the command tail with leading spaces skipped. You should
decode the arguments and set the option as appropriate. If the command tail is not
correct, you should return with V set and R0...
R0 = 0 Bad configure option R0 = 1 Numeric parameter needed R0 = 2 Configure parameter too large R0 = 3 Too many parameters R0 > 3 R0 is a pointer to an error block for Configure to return.
Help
refers to a piece of code to call for that keyword, instead of an
offset of a text string. The code is called as follows:
On entry: R0 = Points at a buffer. R1 = Buffer length. R1 - R6, R12 can be corrupted.On return, if R0 is non-zero, it is assumed to point to a zero-terminated string to pretty-print.
*Help Station
provided by the Econet module, which
returns the current station number of the machine.
&406C0 +0 Hourglass_On +1 Hourglass_Off +2 Hourglass_Smash +3 Hourglass_Start +4 Hourglass_Percentage +5 Hourglass_LEDs +6 Hourglass_ColoursThe Hourglass module's SWI chunk is &406C0. The module can provide 64 SWIs, but it only provides seven. If you tried to call an invalid SWI in the Hourglass chunk, then it would return an error:
>SYS &406CC SWI value out of range for module Hourglass >
On entry: R0 - R8 are passed from SWI caller by RISC OS. R9 = Corrupted on entry. R11 = SWI number, in range 0 to 63. R12 = Pointer to private word for this instantiation. R13 = Supervisor stack. R14 = Contains the flags of the SWI caller. On exit: R0 - R8 are returned to SWI caller by RISC OS. R10 - R12 may be corrupted. Processor status: IRQ is in the same state as it was when SWI called. FIQ is enabled. Processor is in SVC mode. Re-entrancy: Your module may issue SWIs to itself. If it does, it must handle them. 32-bit issues: See text.Your code should not explicitly enable interrupts, if they were disabled on entry; as there is likely to be a good reason for this (ie, your SWI was called from an interrupt handler!).
You must preserve R14 if you plan to call any SWIs as you will be called in SVC mode.
When your handler is called, the SWI number is reduced to a number from zero to sixty three and passed in R11. You should deal with the checking as quickly as possible. Suggested code (from the RISC OS 3 PRM) is:
.SWIentry LDR R12, [R12] ; get workspace pointer CMP R11, #(EndOfJumpTable - JumpTable) / 4 ADDLO PC, PC, R11, LSL #2 ; dispatch if in range B UnknownSWIerror ; unknown SWI .JumpTable B MySWI_0 B MySWI_1 ..... B MySWI_n .EndOfJumpTable .UnknownSWIerror STMFD R13!, {R14} ADR R0, ErrToken MOV R1, #0 MOV R2, #0 ADR R4, ModuleTitle ; from module header SWI "XMessageTrans_ErrorLookup" LDMFD R13!, {R14} ORRS PC, R14, #Overflow_Flag .ErrToken EQUD &1E6 ; same as system message EQUS "BadSWI" ; token to look up EQUB 0 ALIGNThis code is not 32-bit compliant!
R14 contains the flags of the SWI caller, except V has been cleared. To return without updating
the flags, use:
MOVS PC, R14
Otherwise alter the link register with code such as:
ORRS PC, R14, #Overflow_Flag
Note that this is a 32-bit unfriendly operation.
Note that all flags returned are passed on to the caller, so conditional code should be written
with this in mind.
Bit 17 in the given SWI number is irrelevant. The module will be called with a range 0 to 63, and it will not know if the X-form SWI has been called. You should treat it as if X has been set, and thus set R0 and return with V set upon an error occuring. Any error returned is either passed on to the caller (if X set) or dealt with by RISC OS possibly terminating the caller task (if X clear).
On 26-bit systems, R14 is a return address (inside the kernel) with the user's NZCIF flag, V
clear, mode set to SVC. On exit, the current NZCV flags are then passed back to the SWI caller.
Hence, MOVS PC, R14
preserves the SWI caller's NZC flags and clears V. The NZ and
C flags on entry are undefined (and are NOT the caller's), and V is clear. Flag preservation and
manipulation is simple.
On 32-bit systems, R14 is a return address only. There is no way of determining the caller's flags, so you are not expected to preserve them. The NZC and V flags you exit with will be passed back to the caller. This isn't as horrid as it sounds, as it wasn't possible to specify SWI flags on entry in BASIC or most of the C libraries.
If you are writing a new module, simply specify that all your SWIs corrupt flags. Then you can
return with MOV PC, R14
regardless of the mode you are operating in.
If you are converting a module, it is recomended that the same binary work on 26-bit systems.
You should preserve flags in 26-bit mode, if you did so previously. On a 32-bit system, you
need not worry about preserving flags.
When OS_SWINumberFromString or OS_SWINumberToString are called, then if the SWI decoding table is present then the string required is read and the conversion performed. If the SWI decoding table offset is zero, then the SWI decoding code entry is called.
The table format is:
SWI group prefix (usually the module name or something similar) Name of the 0th SWI (null terminated) Name of the 1st SWI (null terminated) ... Name of the nth SWI (null terminated) Null byte to terminate.The strings are NOT word aligned.
You'll have noticed that all SWIs are in the form of a service description, then an underscore,
then a facility description, ie Wimp_CreateIcon. Thus, we are requesting the CreateIcon
facility of the Wimp service. In this case, "Wimp" would be the group prefix and
"CreateIcon" would be one of the SWIs available.
RISC OS will convert this to the familiar Service_Facility format for us.
The ShellCLI module provides us with two SWIs, Shell_Create and Shell_Destroy. Thus it's SWI decoding table would be:
EQUS "Shell" EQUB 0 EQUS "Create" EQUB 0 EQUS "Destroy" EQUB 0 EQUB 0In the case of OS_SWINumberToString, if we passed &405C1 then it would be converted into Shell_Destroy (SWI base chunk &405C0, second SWI). If bit 17 was set (ie, &605C1) then an X would be prepended to give us XShell_Destroy.
If the SWI decoding table does not have enough entries for the requested SWI, then it will be tried in decimal.
It is not acceptable to leave a blank for any SWIs you don't wish to be callable by name (ie, for
registration functions or the like). You may then either use a sequential number, so you SWI
would become something like MyModule_12, or you could use "NOP". More than one
SWI may be 'NOP', so the SWI can only be called by number.
For example, the complete set of SWIs supported by RAMFS is:
RAMFS_ DiscOp NOP Drives FreeSpace NOP DescribeDiscThe reason for the two NOPs is due to the way FileCore handles compliant filing systems.
On entry: R12 = Pointer to private word for this instantiation. R13 = Supervisor stack. R14 = Return address. Text to number: R0 = Any number less than zero. R1 = Pointer to string to convert (control terminated). Number to text: R0 = SWI number ANDed with 63 (ie, offset from 0 to 63). R1 = Pointer to output buffer. R2 = Offset within output buffer at which to place text. R3 = Size of buffer. On exit: R12 preserved. Text to number: R0 = Offset into chunk if SWI recognised, else < 0. R1 - R6 preserved. Number to text: R2 = Updated by length of text. R0 - R1, R3- R6 preserved. Processor status: IRQ and FIQ are enabled. Processor is in SVC mode. Re-entrancy: Entry point is not re-entrant. 32-bit issues: Unchanged; flag preservation not required, only V is examined on exit in number to text conversionThis is used when a SWI is not defined in the SWI decode table (or when the table does not exist). If the SWI cannot be decoded and there is no table, then return with registers unaltered and RISC OS will provide a suitable default.
When converting from number to text, RISC OS will append a null at the position after the length you have returned (in R2).
As a small observation, I can imagine it may be useful if you plan to craft a little bit of
magic, but for the main course it is probably easiest to just use the decoding table facilities
provided by RISC OS.
I have just examined over a hundred modules in my !System directory. Only two modules have
SWI decoding code:
MOVS PC, R14
so it is probably more likely to be
just a null entry rather than anything else.
Bit 0 : Module is 32-bit compatible All other bits reserved (thus, should be zero).Modules that are not 32-bit compatible will not be loaded by a 32-bit version of RISC OS. If the flags word is not present, it is assumed that the module is 26-bit.