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.