Skip to content

Interfacing to PC Assembler (I)

Vector 4:2, page 107

system interfaces performance

Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 107

Abstract

Why use Assembler when we have APL? How can we use Assembler from APL*PLUS/PC? If you use APL*PLUS/PC and if you have a need for better response time, this paper may be of interest.

Introduction

The APL language does not allow us to do things in the best conditions. For instance, a table lookup is a relatively slow operation in APL. The principle is to compare each character from the table with the corresponding character from each row of the matrix. The total number of comparisons is very high. We can reduce this number with a better use of APL, but the fastest APL code will still do too many comparisons, unless it loops and is therefore very slow.

It is easy to write the same function in Assembler. This function will be able to compare a line of the matrix to a line of the table with only one comparison if their first characters differ. This solution will be much faster for big tables.

Another point is that APL needs a table and a matrix with the same number of columns. An Assembler routine can avoid this problem.

An Assembler utility function may be written in less time than necessary to optimize the APL equivalent function, and the result is better.

What may be the drawback of Assembler? One may think that development time is much larger than with APL. In the case of little functions, like a table lookup, we can see that an Assembler routine is harder to write than a bad APL function, but easier than an optimized APL function.

The Assembler routine is harder to maintain than an APL function. Obviously, we will avoid to modify the Assembler code. If we write a utility function in Assembler, this routine will be independent of the workspaces in which it is used.

But be sure to keep a copy of the equivalent APL code, because you may need to transfer your APL workspace to another computer or another APL interpreter, and the Assembler routines will be of no use. In the worst case, you will just have to install your APL code back in the workspace.

So, writing little Assembler routines may decrease the development time and optimize the response time. The maintenance will be about the same. So, why neglect the Assembler solution?

Executing Assembler from APL*PLUS/PC

Now, let us take a look at a more technical aspect.

The APL*PLUS/PC can execute an Assembler written routine with ⎕CALL or ⎕INT. ⎕INT generates a DOS interrupt. So, you can create a resident program, which you will load before APL, and this program will be activated by an appropriate ⎕INT. This is a relatively complex method. On the other hand, you can use ⎕INT for operations that are not directly accessible with APL, like testing the printer state. The available calls to ⎕INT are described in the DOS technical reference and in the technical documentation of the computer you are using.

⎕CALL, too, allows you to execute an Assembler routine. For instance:

⎕CALL 203

The program, which code is represented by the number 203, is executed. This code means ‘return’. So we run a routine that does nothing. We can create an utility function that does nothing:

     ∇ NOTHING
[1]    0 0 ⍴ ⎕CALL 203
     ∇

This utility function is named NOTHING. It may be copied from a workspace to another one, and is self-sufficient: the program ( 203 ) is contained in the function, no disk read is necessary. It is used exactly the same way as APL functions in the workspace. There is no way for the user to know that the program uses Assembler, unless he looks at the program code.

How can we write this Assembler routine that does nothing? Here is its code:

code     segment byte        ; defines a segment named "code"
         assume  cs:code     ; the 8088 register CS contains this value
nothing  proc    far         ; the procedure's name is "nothing"
         ret                 ; return instruction
nothing  endp                ; end of the procedure "nothing"
code     ends                ; end of segment "code"
         end                 ; end of assembler code

So we first have to create a file, having an .ASM (for instance NOTHING.ASM). This file contains the instructions described above. Of course, the comments may be suppressed.

We have to assemble the program, which we do in the following way (using a macro assembler MASM)

MASM NOTHING;

This command creates a file named NOTHING.OBJ. We now have to link this file:

LINK NOTHING;

Now we have a file named NOTHING.EXE. Take care: this file cannot be loaded from DOS. If we wanted to run this routine from DOS, we have to add specific instructions, like an explicit return to DOS. When you type the above commands, you can see an error message given by LINK. In fact, it is only a warning, and if we want to use the routine from APL, we need this warning. The NOTHING.EXE file is not yet useable from APL: we will transform it in binary.

EXE2BIN NOTHING

We now have a set of 4 files, named NOTHING, with different extensions:

NOTHING.ASM
NOTHING.OBJ
NOTHING.EXE
NOTHING.BIN

The files NOTHING.OBJ and NOTHING.EXE will not be used for now. We can load APL and read the file named NOTHING.BIN. Once in APL:

'NOTHING.BIN'⎕NTIE ¯1            ⍝ Open the binary file
CODE←⎕NREAD ¯1 82,⎕NSIZE ¯1      ⍝ Read the whole file

If you look at the CODE variable, you see a semi-graphic character. It is the 204th character of the atomic vector, or ⎕AV indexed by 203 in 0 origin. You can now do the following:

      ⎕CALL CODE
0 0 0 0 0 0 0

This instruction does not do anything, but the result is a 7 zeroes vector ! In fact, these are the values of 7 registers: AX, BX, CX, DX, BP, SI and DI. The Assembler routine did not modify these values, so we get their initial values back. ⎕CALL accepts a left argument, which is a scalar or a vector of up to 7 integers. These values are passed to the above mentioned registers and these registers contents are returned by ⎕CALL at the end of execution. For instance, if we want to initialize the CX register with the value 15, and the others with 0, we can use ⎕CALL with a left argument of 3 elements:

      0 0 15 ⎕CALL CODE
0 0 15 0 0 0 0

The CX register has kept its initial value.

Now, let us create the APL function ‘NOTHING’. Actually, the APL function can contain the binary code in a character format, as the code does not include any special character, as a BEL or FF. In a more general case, this method is impossible, so we have to represent the binary in another format. The floating point format is also unusable, as the least loss of precision on the less significant digit of a real number would destroy the binary code. The best representation of a binary code in a function is the integer format.

In order to convert the character string into integers, we have to be sure that the number of characters is even. If it is not, we need to add another character to it. ⎕TCNUL (value 0) is the one we will use. In our case:

CODE←163 ⎕DR CODE,⎕TCNL

CODE now is an integer, its value is 203. ⎕CALL CODE is still valid. It is now easy to create the APL function ‘NOTHING’.

Summary

This is only the beginning. In fact, an Assembler routine will often have to read one or more APL variables, and give, as a result, another APL variable. The first step to access to an APL variable is ⎕STPTR. This is the subject of the next paper.

Here is an example of an APL function that checks the number of 1 in an APL boolean vector. The result is 1 if there is exactly one 1, otherwise result is 0.

     ∇ R←BOOL1 A
[1]   ⍝ R←1=+/A  A binary scalar or vector
[2]   ⍝          Wrong result if A does not contain only 0's and 1's.
[3]   ⍝          In that case : R ← 1=+/1=A
[4]    R←''⍴(⎕STPTR 'A')⎕CALL ¯10101 1934 ¯24282 6 572 4469
       2751 9728 3723 8 ¯9429 ¯896 29697 31753 ¯13054 ¯18176 1
       20303 3555 440 ¯3584 30127 17158 ¯20494 373 13123 ¯31808
       507 373 ¯13504
     ∇

The Assembler code contained in this function will be detailed in the next paper, to appear in VECTOR 4.3 (January 1988)