rgbds icon indicating copy to clipboard operation
rgbds copied to clipboard

Replace __LINE__ and __FILE__ with something that handles inclusions and repetitions

Open im-mi opened this issue 1 year ago • 5 comments

These preprocessor symbols are a lot more useful than #1068 and #1072 would have you believe. Here are a few examples*:

* Tested with RGBDS 0.5.2, so they might not work exactly the same in other versions. Also note that many of the macros here require that a symbol named DEBUG be defined (otherwise they function as no-ops).

Anonymous Sections

With anonymous sections, you can create a section without having to give it a name. It's a lot more convenient than having to maintain unique section names. This is especially true if you like to put each function into its own section so that the linker can rearrange things more effectively.

This macro will create a section with an auto-generated name. The name is generated using __FILE__, among other preprocessor symbols.

;* Defines an anonymous section.
;* Uses the same arguments as the "section" command.
macro makeSection
    def file\@ equs __FILE__ ; Remove quotes
    section "__anonymousSection_{s:file\@}\@", \#
    purge file\@
endm

Example usage:

    makeSection romx[$4567], bank[2]
myFunction::
    ; << code here >>
    ret

This results in the creation a section with a name something like __anonymousSection_engine/entity.z80_u38. Also note the required indentation before makeSection (it is a macro, after all).

Debug Logging

Emulators such as BGB allow you to log messages to the emulator's internal debug console. This is done by using a special instruction/data sequence. Here are some macros for outputting to BGB's debug console. It makes use of anonymous sections so that the actual string data gets stored elsewhere in a non-fixed bank.

;* Writes a line of text to the debug output.
;* If DEBUG is not defined, then no action is taken.
;*
;* @param strSource A label referring to the null-terminated string that will be written.
macro debug_writeLineAt
    if def(DEBUG)
        ld d, d
        jr .debug_writeLineAt_stringData_end\@
        dw $6464
        dw $0001
        dw (\1)
        dw bank(\1)
        .debug_writeLineAt_stringData_end\@
    endc
endm

;* Writes a line of text to the debug output.
;* If DEBUG is not defined, then no action is taken.
;*
;* @param str A string constant that will be written.
macro debug_writeLine
    if def(DEBUG)
        pushs
            makeSection romx
            __string_\@: db \1, 0
        pops
        debug_writeLineAt __string_\@
    endc
endm

Example usage:

myFunction:
    debug_writeLine "This is a debug message."
    ret

Ouput: This is a debug message.

Error Handling

Here, anonymous sections and debug_writeLine are used together with __FILE__ and __LINE__ to print an error message to the debug console and then hang the program. Another variant of debug_fail exists with a condition code argument, but I feel it's a little too verbose to include here.

macro debug_break
    if def(DEBUG)
        ld b, b
    endc
endm

;* Prints an error and halts the program.
;* @param message The message to print.
;* @param [lineNumber]
;*        The current line number.
;*        This should take the form {__LINE__} (including the curly braces).
macro debug_fail
    if def(DEBUG)
        if _NARG == 1
            ; Remove quotes.
            def message\@ equs \1
            debug_writeLine "Error at {s:__FILE__}: {s:message\@}"
            purge message\@
        elif _NARG == 2
            ; Remove quotes.
            def message\@ equs \1
            def line\@ equ \2
            debug_writeLine "Error at {s:__FILE__} line {u:line\@}: {s:message\@}"
            purge line\@
            purge message\@
        else
            fail "Invalid number of arguments."
        endc
        
        di
        .debug_fail_failureLoop\@
            debug_break
        jr .debug_fail_failureLoop\@
    endc
endm

Example usage:

myFunction:
    debug_fail "Oops! Something went wrong.", {__LINE__}
    ret

Output: Error at "Engine/Map.z80" line 681: Oops! Something went wrong.

Note that the line number preprocessor symbol must be written out at the call site as shown above. Otherwise the line number reported by the error message may not be correct. This is the one caveat of using this method.

... anyway, that's all for now.

im-mi avatar Dec 30 '22 08:12 im-mi

Doing def file\@ equs __FILE__ / ... / purge file\@ is a clever way to get at the "real" filename in a macro, but it's still surprising and inconvenient compared to how __FILE__ works in C. And __LINE__, as you found, really does have to be passed as a literal {__LINE__} argument every time, which is no better than just writing your own line identifier every time:

image

If you want to use real filenames in a macro, you could instead put redef FILENAME EQUS "foo.asm" at the top of every file whose name you care about, and just use FILENAME as-is inside macros.

Rangi42 avatar Dec 30 '22 13:12 Rangi42

In the case of "just writing your own line identifier every time", you'd need some sort of preprocessor to change the literal 840 every time a line is added or deleted above line 840 so that the value remains correct.

pinobatch avatar Dec 30 '22 14:12 pinobatch

Rather than un-deprecating these, let’s add new versions that act the way we want? Specifically a line constant that tracks includes and repetitions

eievui5 avatar Dec 30 '22 14:12 eievui5

Rather than un-deprecating these, let’s add new versions that act the way we want? Specifically a line constant that tracks includes and repetitions

I have to admit this is probably a better course of action. And it would remove the need for weird workarounds.

im-mi avatar Dec 30 '22 23:12 im-mi

For functions, a simple "unique name" is, by necessity, the function's:

SECTION "MyFunction", ROM0 ; (Aside: using this pattern for ROM0 functions is a good idea, but ROMX is not.)
MyFunction::
    ; snip
    ret

For those debug messages, and for a unique name across the whole program, __FILE__ is ill-advised:

$ bat a.asm b.asm common.inc 
───────┬───────────────────────────────────────────────────
       │ File: a.asm
───────┼───────────────────────────────────────────────────
   1   │ INCLUDE "common.inc"
───────┴───────────────────────────────────────────────────
───────┬───────────────────────────────────────────────────
       │ File: b.asm
───────┼───────────────────────────────────────────────────
   1   │ INCLUDE "common.inc"
───────┴───────────────────────────────────────────────────
───────┬───────────────────────────────────────────────────
       │ File: common.inc
───────┼───────────────────────────────────────────────────
   1   │ MACRO make_section
   2   │     def anon_file\@ equs __FILE__
   3   │     SECTION "Anonymous section {anon_file\@}", \#
   4   │     purge anon_file\@
   5   │ ENDM
   6   │ 
   7   │     make_section ROM0
───────┴───────────────────────────────────────────────────
$ rgbasm a.asm -o a.o && rgbasm b.asm -o b.o && rgblink a.o b.o -m /dev/stdout
warning: a.asm(1) -> common.inc(7) -> common.inc::make_section(2): [-Wobsolete]
    `__FILE__` is deprecated
warning: b.asm(1) -> common.inc(7) -> common.inc::make_section(2): [-Wobsolete]
    `__FILE__` is deprecated
error: Section name "Anonymous section common.inc" is already in use

What would be useful would be the output file name... and even then, it's iffy. (The root source file name may not be enough, for example if I run rgbasm -o v1.o configurable.asm -DVERSION=1 and rgbasm -o v2.o configurable.asm -DVERSION=2, though maybe that's niche enough that we can ignore it.) I'd rather advocate for some kind of unique name passed by the build system, e.g. -DUNIT_NAME=main.

ISSOtm avatar Jan 07 '23 19:01 ISSOtm