by Richard Russell, December 2008
This article supplements the information in the main Help documentation under Conditional assembly and macros.
The assembler built into BBC BASIC for Windows doesn't have a conventional macro facility, but something similar can be simulated utilising the OPT pseudo-instruction. OPT is normally used to control the assembler options (for example whether a listing is generated) but in this application it has two useful features: firstly it generates no code and secondly it takes a numeric parameter. This provides a means of calling a user-defined function from within the assembler, and if that function itself outputs code (or data) it can behave like a macro.
One common use of a macro is to insert a block of code that you use frequently. Suppose your program often wants to find the absolute value of the signed 32-bit number in the eax register, but you don't want the overhead of making a subroutine call to do so. The following code simulates a macro for the purpose:
DEF FNabs [OPT 0 cdq xor eax,edx sub eax,edx ] = pass%
Now, whenever you want to calculate the absolute value of eax you can do so in your main assembler code as follows:
DIM code% 100, L% -1 FOR pass% = 8 TO 11 STEP 3 P% = code% [OPT pass% ... ; code which puts a signed value into eax OPT FNabs ... ; more code OPT FNabs ... ; etc. ] NEXT pass% END
Here FNabs returns the current value of pass%, so in the main assembler loop the OPT FNabs pseudo-instructions simply set the assembler options (to what they already were). However the called function itself emits the opcodes corresponding to the absolute value calculation. You can see the effect by looking at the listing generated (note that the OPT 0 in FNabs prevents any listing being generated there):
1002181A OPT pass% 1002181A ; code which puts a signed value into eax 1002181A 99 33 C2 2B C2 OPT FNabs 1002181F ; more code 1002181F 99 33 C2 2B C2 OPT FNabs 10021824 ; etc.
Another way in which this facility can be used is to extend the assembler to handle instructions which are not supported natively. For example the instruction SYSENTER is available only on the Pentium Pro and later range of processors, and is therefore not supported in the BBC BASIC for Windows assembler. The following user-defined function can be used to 'add' that instruction:
DEF FNsysenter [OPT 0 DB &0F DB &34 ] = pass%
Now, whenever you require to call the SYSENTER instruction, you can do that in your assembler code as follows:
A similar technique can be used when the instruction takes a parameter, for example a register name:
DEF FNfcmovb(reg$) [OPT 0 DB &DA DB &C0+VALRIGHT$(reg$) ] = pass%
This implements the FCMOVB instruction which takes as a parameter an FPU register name (st0 to st7). The routine automatically adjusts the emitted opcode according to the register specified (for simplicity the operand is not checked for validity, but it could be):
With care the technique can be extended to instructions which take more complex operands.
If you need to use labels in the assembler code generated by a macro, care is needed to prevent a Multiple label error occurring when the macro is called more than once. If the labels are genuinely 'local' to the macro (this implies only 'backwards' references) then you can simply declare them as LOCAL:
DEF FNincarray LOCAL label [OPT 0 .label inc dword [ebx] add ebx,4 loop label ] = pass%
Often you can rearrange code to ensure all references to labels are 'backwards' (i.e. the label is defined before the reference to it) but sometimes 'forward references' are unavoidable. In such cases the macro must be adapted so that a different label is created each time it is used. The easiest way of achieving that is to use an array element as the label:
DEF FNsimplisticabs(i%) PRIVATE label() LOCAL label DIM label(10) label = label(i%) [OPT pass% AND &E test eax,eax jns label neg eax .label ret ] label(i%) = label = pass%
Here the macro takes as a parameter an 'instance' value which is different for each use of the macro in your program:
DIM code% 100, L% -1 FOR pass% = 8 TO 11 STEP 3 P% = code% [OPT pass% ... ; code which puts a signed value into eax OPT FNsimplisticabs(0) ... ; more code OPT FNsimplisticabs(1) ... ; etc. ] NEXT pass% END
As shown, the macro can be called up to ten times from the program (0 to 9); if this is insufficient the size of the label() array may be increased.
If it's inconvenient to pass an 'instance' value each time the macro is called (and to ensure they are all different) this may be automated by checking whether the value of pass% has changed, and if so initialising the array subscript:
DEF FNsimplisticabs PRIVATE label(), p%, i% LOCAL label DIM label(10) IF p%<>pass% p% = pass% : i% = 0 label = label(i%) [OPT pass% AND &E test eax,eax jns label neg eax .label ret ] label(i%) = label i% += 1 = pass%
Note: Although the interpreter accepts the direct use of array elements as assembler labels, which would allow the above code to be slightly simplified, this is not strictly valid syntax and is not permitted by the compiler.