Blog

Relearning MSX #23: Calling assembler routines from MSX-C

Posted by in How-to, MSX, Retro, Technology | September 05, 2015

In the previous post we learnt a couple things about how MSX-DOS manages files and runs programs.

This post is more technical: we’ll see how to use assembler routines from MSX-C.

This subject is a bit more advanced that what we’ve seen so far. Feel free to skip this chapter for the time being if you don’t need to mix C and assembler yet. The reason why we’re going to see this now is that there are advanced users playing around with MSX-C who have asked how to do this.

Standard types in MSX-C

First of all we need to understand how the standard MSX-C data types are stored in memory.

16-bit values are always stored in memory with the low order byte first (little endian), and the high order byte in the memory address after it.

There’s also a couple things we have to keep in mind:

  • In MSX-C pointers are always 16-bit values, the same as integer values
  • The numeric types short and int are exactly the same thing
TypeLength (bits)Range
char80 to 255
short16-32768 to 32767
int16-32768 to 32767
unsigned160 to 65535

How MSX-C passes parameters to assembler routines

When coding our own assembler routines some of them won’t require any parameters (for example, a routine to clear the screen), some will require a fixed number of parameters (a routine that changes the screen colors), and from time to time some will require a variable number of parameters (a printf()-like function).

Let’s see each of these cases one by one:

1) Routines that don’t require parameters

This case is very, very simple. Just implement a routine in assembler, and make sure that it’s defined as a public symbol. In most assemblers this is achieved with the public keyword (we’ll be using the M80 syntax). Then we just need to declare the function in MSX-C with the same name it has in assembler.

Consider the following assembler routine as an example. The code is stored in a file called HELLO.MAC:

Contents of the file HELLO.MAC. (Click to enlarge)

Contents of the file HELLO.MAC. (Click to enlarge)

This code doesn’t take any parameters, and it doesn’t return anything. It just prints a message on the screen (“Hello from assembler!”). Note that we have to make the routine public, or we’ll be unable to link it to the C program.

Using this assembler routine from C is simple: just declare the function at the beginning of the C program. See this example (file name TEST1.C):

test1_c

TEST1.C. This C program uses the hello() function implemented in HELLO.MAC (Click to enlarge)

Notice this line next to the beginning of the file:

VOID    hello();

This tells the compiler that the function hello() exists, that it doesn’t take any parameters, and that we’ll ignore any values returned by it (if any).

Next we need to assemble the hello() routine (in HELLO.MAC), compile the C program (TEST1.C), and link them together. We could run each command manually, but this would be annoying if we’re developing and have to assemble, compile and link again and again. It’s just easier to make a batch script to handle this. Let’s call it C1.BAT and create it with these commands:

c1_bat

C1.BAT. (Click to enlarge)

This script first assembles the HELLO.MAC routine using the M80 assembler, then compiles and assembles the C code (CF/CG/M80), then links both of them together in an executable called TEST1.COM, and finally removes all temporary files.

The resulting program (TEST1.COM) is written in C, but prints text on the screen using an assembler routine:

test1

Results of running TEST1.COM (Click to enlarge)

2) Routines that require a fixed number of parameters

This case is also very simple. We saw at the beginning of this article that char is an 8-bit type (as well as all types that are a redefinition of char, like VOID, STATUS, BOOL or TINY), and short/int and unsigned are 16-bit. Pointers are also 16-bit.

The way parameter passing works is very simple:

  • If the first parameter is an 8-bit type then it goes into the A register. Else, it goes into HL.
  • If the second parameter is an 8-bit type then it goes into the E register. Else, it goes into DE.
  • If the third parameter is an 8-bit type then goes into the C register. Else, it goes into BC.
  • If there are more parameters, they’re passed in the stack, always as 16-bit values. For 8-bit arguments the high byte is undefined.
  • Finally, the top of the stack contains the return address (where the assembler RET instruction will return).

If the assembler routine uses up to three parameters then the code is very simple: just take the parameters from the appropriate register(s), and then finish with a RET. If there are more parameters then our routine will have to take care of restoring the stack pointer before returning (see the next section about how to do this).

Let’s see an example of a routine that takes a single parameter. This routine takes as a parameter a pointer to a text string, and prints it on the screen using MSX-DOS’ STROUT system call (09h). Save it to a file called HELLO2.MAC:

hello2_mac

Code of HELLO2.MAC. (Click to enlarge)

This routine prints on the screen the text “You said: “, followed with whatever string it was passed as a parameter. Using it in MSX-C works in exactly the same way as in the previous case. See this test program, TEST2.C:

test2_c

TEST2.C (Click to enlarge)

Note that even though the hello2() routine takes a parameter the function declaration doesn’t mention it. This is because we’re working with K&R C, which didn’t include the arguments in function declarations.

Also note that every time we call hello2() we pass a string that ends in a $ sign. This is because hello2() prints text using the MSX-DOS system call STROUT, which takes a $ sign as the end of the string.

Again, we use a batch script to assemble, compile and link (C2.BAT):

c2_bat

C2.BAT (Click to enlarge)

Running the TEST2.COM program produces this output:

test2

Output resulting from running TEST2.COM (Click to enlarge)

3) Routines that accept a variable number of parameters

In this case MSX-C passes all the parameters on the stack, and register HL contains the number of parameters. All parameters occupy 2 bytes in the stack, so if we pass an 8-bit value then the high order byte would be undefined (that’s why we have to cast to int when printing char values using printf()).

As in the previous case, the value in the top of the stack is the return address, so we have to be careful to save it for later, and also restore the stack pointer to the address it was when MSX-C called the assembler routine.

This time let’s see the C program first (TEST3.C). This program calls the many() assembler routine, that takes any number of strings and prints them one after the other:

test3_c

TEST3.C (Click to enlarge)

The important thing to note here is how we declare the function:

VOID    many(. );

K&R C doesn’t support functions with a variable number of arguments, but MSX-C does. The dot inside the parentheses tells the compiler that this function can take any number of arguments. If we don’t declare it in this way then MSX-C will pass parameters as in case 1) or 2), depending on how many there are, and our assembler routine won’t work.

Let’s see the assembler part next. This is the source code for the many() assembler routine (MANY.MAC). This code is slightly more involved because it has to take care of a few more things:

many_mac_commented

MANY.MAC (Click to enlarge)

This is what the routine does:

  1. Check the number of parameters. If there was none, print “No parameters.” and return. In this case there’s no need to do anything with the stack pointer.
  2. If there are parameters, the first thing to do is save the return address and the stack pointer in the variables retadd and stack, respectively.
  3. Next, the routine enters a loop (from label loop to the instruction jr nz,loop). In each iteration of the loop it takes a string pointer from the stack (pop de) and calls MSX-DOS system call STROUT to print it on the screen.
  4. When there are no more parameters, prints a carriage return and a line feed (label CRLF).
  5. Finally, the routine restores the stack pointer, puts the return address back on the stack and returns.

Note that in this particular case we’re only handling up to 255 arguments for simplicity: HL contains the total number of arguments, but we ignore H.

Assembling, compiling and linking works in exactly the same way as before. Use this C3.BAT script:

c3_bat

C3.BAT (Click to enlarge)

Running TEST3.COM produces this:

test3

Results of running TEST3.COM (Click to enlarge)

Returning values from assembler routines

This is very, very simple. If the assembler routine returns an 8-bit value, then it goes into the A register. If it returns a 16-bit value, it goes into HL.

See this example (SUM8.MAC):

sum8_mac

SUM8.MAC (Click to enlarge)

This very simple sum8() routine takes two 8-bit values, adds them and returns the result. The first input parameter is in register A and the second is in register E (see again the description of how parameter passing works for routines with a fixed number of arguments). Then we add them using the ADD instruction, and return leaving the result in register A.

In this particular case there could be overflow if the sum of both values is higher than 255. I left it as it is for simplicity.

This is the corresponding C program (TEST4.C) that uses this routine:

test4_c

TEST4.C (Click to enlarge)

This program declares a couple of char variables to use in the loops, and another one to hold the result of their sum. Notice that the declaration of the sum8() function tells the compiler that this routine returns a value of type char, so it should take the value in register A as the result.

Again, assemble/compile/link with a script identical to the previous ones (C4.BAT):

c4_bat

C4.BAT (Click to enlarge)

This is the result of running the TEST4.COM program:

test4

Output of TEST4.COM (Click to enlarge)

Summary

In this post we’ve seen how to use assembler routines in our C programs. There are three cases, depending on how many parameters they need. We’ve also seen how to return values from our assembler routines to the C program.

In the next post…

We will look at the opposite process: how to call MSX-C functions (such as printf()) from our assembler programs.


This series of articles is supported by your donations. If you’re willing and able to donate, please visit the link below to register a small pledge. Every little amount helps.

Javi Lavandeira’s Patreon page

Leave a Reply

Your email address will not be published. Required fields are marked *