Creating API docs for assembly language projects

illustrations illustrations illustrations illustrations illustrations illustrations illustrations

Published on 21 April 2022 by Andrew Owen

post-thumb

This week, I’m releasing what I hope will be the penultimate beta of SE Basic IV (an open source classic BASIC interpreter). The last beta should contain the missing sound and graphics functionality, and then it should go to a release candidate.

One of the things I’ve spent a lot of time considering is the public API. The underlying operating system has its own API, but it’s rather primitive and hard to change for reasons (because the OS was reverse engineered and is barely documented).

Back when I was a full-time API writer, I used Swashbuckle to generate OpenAPI docs from comments in the C# source code. I’d previously used Doxygen to do the same for C++ and Javadoc for Java. I wanted something that would do the same for assembly language. And I found it, in the shape of a Perl script called asmdoc by Bogdan Drozdowski.

The script parses a set of assembly files looking for comments in a particular format, with a known set of tags such as @param, @return, @throws. It generates a set of HTML files including a table of contents, index, list of constants, a help page and so on.

The script was written a long time ago and the default appearance is very web 1.0. But the code includes an embedded CSS file that I was able to edit to give the output a much more modern look, inspired by ReDoc. I only had to make a single change to the code to get it to work with the current version of Perl (at time of writing).

The script is self-contained and will produce all the required output files. You can grab it from: https://github.com/cheveron/sebasic4/blob/main/api/asmdoc.pl. And it’s free to use in your own projects. I contacted Bogdan to thank him for his work and show him what I’d done. He was surprised that I hadn’t needed to alter his code.

I created a Git Action to build the API docs and publish them whenever there’s a code change in the main branch. The API docs are hosted on the project’s GitHub Pages (which use Gatsby). Here’s the script:

name: API
on:
  push:
    branches: [ main ]
  workflow_dispatch:
    jobs:
      asmdoc:
        runs-on: ubuntu-latest
        permissions:
          contents: write
        steps:
          - uses: actions/checkout@v2
          - name:
            run: |
              cd api
              perl asmdoc.pl -author -version ../basic/basic.inc ../basic/modules/*.asm
              cd ..
              mkdir ../temp
              cp api/* ../temp        
              git config user.name github-actions
              git config user.email github-actions@github.com
              git fetch
              git checkout gh-pages
              cp ../temp/* api
              git add .
              git commit -m "asmdoc"
              git push origin gh-pages              

I’ve covered Git Actions in a previous article. The crucial information here is on line 18 where the files to include are specified (the basic.inc file and all the files with the .asm extension).

After the Perl script is run the rest of the action grabs the output, and then pushes it to the gh-pages repository, which has the effect of publishing it here: https://source-solutions.github.io/sebasic4/api/.

Because the project is open source, I’ve created stubs for the most common routines. However, these should be considered part of the private API. The address of the code may change in a later release, and so the only program that should call these APIs is the firmware itself.

To provide a public API for applications to call where the addresses won’t change, I’ve used a vector table. This is a long list of JP instructions (immediate jumps to other addresses). While the destination of the jumps can change from release to release, the location in the table is fixed in memory. This means that applications can safely call it without worrying that the address will change in future.

With all the underpinnings in place, the next task was to come up with a useful set of functions. I looked at the CP/M and MS-DOS APIs, but they were, understandably, simplistic, and not in a logical order (having grown over time as new versions were released). I eventually took inspiration from OpenStep and BeOS.

For avoidance of confusion, namespaces should always use American English. Then I use:

  1. Two letters (SE).
  2. The domain. For example, File, Folder, Screen, Keyboard and so on.
  3. [Optional] A subdomain. For example, Palette.
  4. The action. For example, Open, Close, Read, Write and so on.
  5. [Optional] A qualifier. For example, Exists.

Here are some example endpoints:

  • SEFileOpenExists
  • SEScreenPaletteSet

And here’s the comment in the code that generates the docs for the first example:

    ;;
    ; open a file from disk for reading if it exists
    ; @param IX - pointer to ASCIIZ file path
    ; @return file handle in <code>A</code>
    ; @throws sets carry flag on error
    ;;
    SEFileOpenExists:
    	jp v_open_r_exists;					// 

And in the API docs, this results in something like this:

SEFileOpenExists
open a file from disk for reading if it exists
Parameter:
IX - pointer to ASCIIZ file path
Returns:
file handle in A
Throws:
sets carry flag on error

Now I’d better get back to preparing the release.