6

Using ca65 assembler, I'd like to use a macro to encode a string as a pascal style string or FORTH counted string with a leading length byte.

In ca65 documentation there is this example:

        .macro  PString Arg
                .byte   .strlen(Arg), Arg
        .endmacro

It works great for one string argument, like "TEST", but how can I use the macro (or how do I need to modify the macro) if I want to be able to mix literal strings and character codes, like for example these two strings:

"Hello", $20, "world"
"Hello world", $0A, $0D

I'm thinking maybe some kind of recursion would be needed?

2 Answers 2

11

TL;DR:

This macro will solve your issues:

        .feature string_escapes
.macro  PString Arg
        .byte   :++ - :+
:       .byte   Arg
:
.endmacro

When called with more than one argument the arguments have to be quoted in curly brackets. Due enabled string-escapes C like strings can be handled as well.

000800  1  03 41 42 43          PString "ABC"
000804  1  0D 48 65 6C      `   PString {"Hello world", $0A, $0D}
000808  1  6C 6F 20 77  
00080C  1  6F 72 6C 64  
000810  1  0A 0D        
000812  1  05 41 42 43          PString "ABC\n\r"
000816  1  0A 0D

Should serve its purpose.


How Come?

Well, the main issue here that"Hello world", $0A, $0D given as parameter to a macro like

.macro  PString1 Arg
        .byte   .strlen(Arg), Arg
.endmacro

is not one but three parameter. So using above macro would yield

Error: Too many macro parameters

as there is only one parameter Arg defined.

So lets see

Attempt #1:

CA65 allows it to quote parameters that include commas using curly braces so the macro would stay the same, but be called like this:

        PString1 "ABC"
        PString1 {"ABC",$0D,"ASDF"}

Looks great, doesn't it? The only drawback here is that .strlen() wants a single string as parameter, so we'll get a

Error: String constant expected

with the second invocation.

Attempt #2:

But hey, CA65 was made as backend for the CC65 C compiler, so it should do the various escape character, shouldn't it? To make it work, the string escape feature needs to be enabled:

        .feature string_escapes

Doing so will nicely transform whatever we need while still only handling strings:

000800  1  0C 48 65 6C          PString1 "Hello\x20world"
000804  1  6C 6F 20 77  
000808  1  6F 72 6C 64  
              
00080C  1  0E 48 65 6C          PString1 "Hello world\n\r"
000810  1  6C 6F 20 77  
000814  1  6F 72 6C 64  
000818  1  0A 0D        

Here are your strings. Cool, isn't it?

Well, no, it works, but looks plain ugly. If one wants to do C, one should use C.

THIS IS ASSEMBLER !

Attempt #3:

We could for example go ahead, define a list of parameters and iterate (*1) along. Something like:

.macro  PString2 Arg1,Arg2,Arg3
        .byte   .strlen(Arg1)+.strlen(Arg2)+.strlen(Arg3),Arg1,Arg2,Arg3
.endmacro

(Simplified version, will only work with three parameters which are strings)

Sounds great at first, except, it does fail when there are more elements than defined. Further, like with multiple parameters, .strlen() does not go well with no parameter either. Not nice, but can be done, the loop will need a few checks for all these cases. Still, there is no (easy) solution for elements like $20 or $0D, as they are not strings.

Of course it might be possible to overcome all of this, but the result would be an overkill for a job that simple.

Attempt #4:

But serious, why not going back to the good old ways of Assembler. Tools proven since the very early days and perfectly good today:

Let the Assembler do the Math

Huh? Isn't the Assembler already doing the job by using .strlen()? True, but lets drop such fancy high level pseudo functions and use what all assemblers always could do well, calculating addresses. Like A minus B. A string's length is in no way different from the difference of its start and end address, isn't it? Lets try this:

.macro  PString3 Arg
:       .byte   :+ - :- , Arg
:
.endmacro

What does it do?

  • : is creating a 'unnamed label'(*2), the most simple version of a label
  • .byte of course defines a sequence of bytes
  • :+ refers to the next unnamed label (here in the next line)
  • :- refers to the previous unnamed label (begin of this line
  • Arg puts in whatever is given as (first) argument.

Core here is to subtract the address the first : label stands for from the second - essentially giving the length of all bytes defined with this line, and so it does:

000800  1  04 41 42 43          PString3 "ABC"
000804  1  0E 48 65 6C          PString3 {"Hello world", $0A, $0D}
000808  1  6C 6F 20 77  
00080C  1  6F 72 6C 64  
000810  1  0A 0D        

Two lines of macro and works like a charm. With .feature string_escapes activated it will even allow to mix in C-Strings:

000812  1  06 41 42 43          PString3 "ABC\n\r"
000816  1  0A 0D        

This macro defines an including length byte (as I prefer it). To get an exclusive one, the labels need to be moved:

.macro  PString4 Arg
        .byte   :++ - :+
:       .byte   Arg
:
.endmacro

Which in turn produces:

000800  1  03 41 42 43          PString4 "ABC"
000804  1  0D 48 65 6C      `   PString4 {"Hello world", $0A, $0D}
000808  1  6C 6F 20 77  
00080C  1  6F 72 6C 64  
000810  1  0A 0D        
000812  1  05 41 42 43          PString4 "ABC\n\r"
000816  1  0A 0D

All of this works as well with ASCIZ strings, which in addition offer the translations of code sets when linking for multiple platforms - but that's a different story.


Final Words (a rant)

While Assembler allows quite abstract approaches, like other HLL (*3), at it's very core, it is a tool to build (assemble) memory content from input data. A fact very easy to forget when thinking in higher abstraction layers like modules, functions, parameters and instructions. But at the bottom, it's all about creating and ordering bytes in linear memory (chunks). LDA #$A2 is nothing else than a fancy way of saying A2 twice.

Many HLL are build in a way that they only transform their source into some (usually primitive) Assembler source and let the Assembler do the dirty work of creating the needed values and relations. Like some upper class slobs with no idea what work it was to create the food the maid brings or how their refuse vanishes after a maid takes it.

Some more modern Assemblers are build with a HLL mindset, CA65 being one of them, offering functions that directly relate to HLL concepts. Essentially shortcuts to pamper the HLL compiler part, so it doesn't have to care what the stuff it outputs means in real world terms. Such functions can be quite nice but quite often single purpose for the language they are made to support.

So long story short: Try to solve an issue with the basic tools of Assembly first, before spending too much time looking on fancy functions :))


*1 - Please repeat loud and clear: NO, Recursion Is Never A Solution

*2 - Using local labels will work as well, but why adding when basic mechanics will do it as well.

*3 - I very much see Assembler on par with average HLL and believe it should be handled as HLL in most regards.

2
  • Thanks a lot, I've learned a lot. Very clever to use unnamed labels and their difference to know the length! I appreciate the detail and the approach with multiple attempts. Jul 12, 2021 at 9:04
  • @AlexandreDumont In assembly everything is about building an image to fill memory, so calculating addresses and relations between them is at the very core of an Assembler. Using unnamed labels is just a special case with CA65 - Kevin Sherlock's nice answer does show well that it works with local or any other kind of label as well.
    – Raffzahn
    Jul 12, 2021 at 13:30
1

ca65 doesn't seem particularly retro to me but you could do it recursively:

    .macro StringHelper p0, p1, p2, p3, p4
    .ifnblank p0
    .byte p0
    StringHelper p1, p2, p3, p4
    .endif
    .endmacro

    .macro  MultiPString p0, p1, p2, p3, p4
    .local begin,end
    .byte end-begin
begin:
    StringHelper p0, p1, p2, p3, p4
end:
    .endmacro

or with an unrolled loop:

    .macro  MultiPString p0, p1, p2, p3, p4
    .local begin,end
    .byte end-begin
begin:
    .ifnblank p0
    .byte p0
    .endif
    .ifnblank p1
    .byte p1
    .endif
    .ifnblank p2
    .byte p2
    .endif
    .ifnblank p3
    .byte p3
    .endif
    .ifnblank p4
    .byte p4
    .endif
end:
    .endmacro

The recursive solution is a lot cleaner as you increase the parameter count.

2
  • 1
    Well, being an assembler for the 6502 8bit processor, I personally think it does count as retrocomputer. Jul 12, 2021 at 7:29
  • 2
    @AlexandreDumont Yes and no. It's rather the fact that CA65 is a widely used tool in retro computing that makes it on topic.
    – Raffzahn
    Jul 12, 2021 at 13:32

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .