2

I have a function that should be callable with the following syntax:

void __fastcall__ initSIDplayer1(uint8_t a);

The __fastcall__ modifier here means that the argument is passed in the Accu instead of the software stack. The address is given as 16-bit address, for example, 0x1000 for a SID player file that is loaded with or before the main program.

I have used the following line to define the function with the specified address:

static void  __fastcall__ (*initSIDplayer1)(uint8_t)=(const void *)0x1000;

The approach works, but the compiler generates a lot of code in comparison to the statically defined function initSIDplayer1:

;
; code generated for initSIDplayer1(1);
;
        lda     #$61
        jsr     _initSIDplayer
;
; code generated for initSIDplayer2('a');
;
        lda     _initSIDplayer2
        ldx     _initSIDplayer2+1
        jsr     pushax
        lda     #$61
        pha
        ldy     #$00
        lda     (sp),y
        sta     jmpvec+1
        iny
        lda     (sp),y
        sta     jmpvec+2
        pla
        jsr     jmpvec
        jsr     incsp2

The generated code for initSIDplayer2 writes the target address into memory (using two stack operations) and then jumps to that memory address-1 where it executes another jmp. This is a very flexible, but slow and cumbersome approach.

How do I need to write the function definition with the given address so that the compiler translates it into a simple jsr ADDR call like with initSIDplayer1?

I know that this can be also done with inline assembler or an external assembler file, but I would be interested in the C code syntax.

For completeness, this is the minimum test program:

#include <stdint.h>

void __fastcall__ initSIDplayer1(uint8_t a);

static void  __fastcall__ (*initSIDplayer2)(uint8_t)=(const void *)0xFFD2;

void main() {
  initSIDplayer1('a');
  initSIDplayer2('a');
}

I compiled it with cc65 function.c which generates an .s file.

7
  • Err, you get a .o file? AFAIR cc65 only delivers a .s (to be later assembled using ca65, wouldn't it? Did you maybe use cl65 instead?
    – Raffzahn
    Commented Jan 18, 2021 at 0:10
  • You're right. The .o file comes from the assembly step. Fixed it in the text, thx.
    – Peter B.
    Commented Jan 18, 2021 at 0:14
  • 1
    Just plunged it into the compiler. It might be helpful to show that the function pointer is really stored as pointer within the initialized data segment (.segment "DATA") as that's why it builds the jump table from before jumping. It essentially ignores the const part. Now we only need to look why.
    – Raffzahn
    Commented Jan 18, 2021 at 0:22
  • Sorry just curious, where is fastcall defined? I haven't seen this type of modifier before. Is it a standard C thing?
    – dashnick
    Commented Jan 18, 2021 at 2:49
  • 1
    Fastcall is a Feature of cc65 a compiler for the 6502 microprocessor. The 6502 does only have a small hardware stack of 256 bytes, so the C implementation makes a software stack to handle function parameters. Operating this stack for call parameters takes a lot of code and time, which is why the fastcall modifier allows to define a function that gets its last parameter passed in the processor registers instead.
    – Peter B.
    Commented Jan 18, 2021 at 9:33

3 Answers 3

6

Preface: I'm not the world greatest C expert - not at least as I dislike C quite a lot :))


What Happenes

The general issue here is that CC65 puts the address into a pointer variable, visible when looking at the generated source:

.segment    "DATA"
_myfunction2:
    .word   $FFD2

Later that variable gets loaded onto the stack - like CC65 does with every 16 bit value - and from there moved to a prepared jump which then is taken. This is kind of ok-ish with variable function pointers, less cool with a constant address.

Solution in C

No idea. Personally I'd write

static void __fastcall__ (* const myfunction2)(uint8_t a) = (const void *) 0xFFD2;

as myfunction2 is a constant, but that should not make a difference and true, it doesn't :(

I played a bit around, but couldn't see any way to change that behaviour. Even with using implied type conversion (adds only a warning) didn't change the result. The same for leaving out the non standard __fastcall__ attribute (A is used anyway for the first parameter).

What I'd do:

The main problem here is that external references, and that's what a ROM function is, aren't really a thing C cares for when compiling a source file. Stitching compiled objects together is what the linker is meant for. So why spare such details from the C compiler and let the linker do it's job?

All the Compiler needs to know is that it's a function and what calling conventions to use, which simplifies above to an (almost) straight:

void __fastcall__ myfunction2(uint8_t a);

The address now needs to be delivered to the linker using command line parameter or in its configuration file. It may be safe to assume that there will be usually more than just one entry point to ROM such a program needs. Going by the address, this looks quite like we have a C64 at hand, right? So lets add a collection of entry points:

; C64_Kernal_Calls.s:
;
; Commodore-compatibles Kernal functions

CHRIN  :=       $FFCF
CHROUT :=       $FFD2

       .export  CHRIN
       .export  CHROUT

Of course this can be beautified using a macro:

; C64_Kernal_Calls.s:
;
; Commodore-compatibles Kernal functions

.macro  entrypoint LBL,ADR
LBL     :=      ADR
        .export ADR
.endmacro

      entrypoint CHRIN, $FFCF
      entrypoint CHROUT,$FFD2

Calling that from C now works rather simple:

void __fastcall__ CHROUT(uint8_t a);

// ...

    CHROUT('a');

Going that road´provides, beside solving the issue, considerable better structuring than embedding addresses into random source files, doesn't it?

Always program as if you're not writing another Hello_World but the next GEOS.

3
  • Thanks for the solution using a seperate Assembler file as a module to contain the call addresses. A bit of a downside is that we need to define a spearate .s file for getting the address in. While this is definitely a good idea for a collection of Kernal calls like in your example, when you think of an externally generated program (like a SID music play routine) that needs to be called, I would prefer to add it directly in a C program instead of writing an empty Assembler file. So I think the question what is a better structure depends on the particular application.
    – Peter B.
    Commented Jan 18, 2021 at 9:47
  • 1
    In your example, you need to export the labels from the Assembler file with a leading underscore (or define them with an underscore in the first place) to match the respective label in C. For example, .export _CHROUT := CHROUT
    – Peter B.
    Commented Jan 18, 2021 at 10:27
  • 1
    @PeterB. Well, even with a small project I'd still use a separate file as it simply is an external code to be called. Also, Such a file would be a standard file for the platform used, collecting all kinds of entry points and reused every time. For assembly projects the same way as for C. In fact, I wouldn't be surprised if CC65 doesn't already include something similar.
    – Raffzahn
    Commented Jan 18, 2021 at 16:26
1

While I've not actually used CC65, I'm familiar with the 6502, and I would expect that if you do something like:

unsigned char springboard[5]; // At file scope
...
... within a function
  ((void(*)(void))springboard)();

the result would be a jsr springboard instruction. If you place 0xA9 and 0x4C into springboard[0] and springboard[2], respectively, the address of the function in springboard[3] and sringboard[4], and the desired function argument in springboard[1], the effect should be to call the desired function with the desired value in the accumulator.

3
  • I just tested it, the effect works, but cc65 again translates it into the indirect approach instead of just placing an "jsr". I also tried a version where the array was declared as static const, which made no difference in the generated code. So it does not solve the problem. I like your approach of writing machine code directly into an array very much though!
    – Peter B.
    Commented Sep 23, 2022 at 20:13
  • @PeterB.: What happens if you write a dummy function which would do something that takes at least two bytes of code plus the return (maybe force a read of $D020 or something), and overwrite the first three bytes of that function with a jmp to the function you want?
    – supercat
    Commented Sep 23, 2022 at 20:21
  • @PeterB.: BTW, another option might be to have one compilation unit contain the array definition, along with code that populates the array, but have another compilation unit contain a function declaration with the same name, and call the function within the latter.
    – supercat
    Commented Sep 23, 2022 at 20:22
1

Thanks for the discussions and suggestions. I summarize here the multiple ways to define a function with a given address as a C function.

Assigning a function pointer

The call described in the question does the trick

static void  __fastcall__ (*myFunction)(void)=(const void *)0xe544;

but, at least in compiler version cc65 V2.19, the line where the function is called

myFunction();

generates code where the address is loaded into AX first and then jumped to by a routine (callax). Given that the address is fixed, the compiler could have made a JSR addr, but it always assumes the pointer to be dynamic.

The compiler also throws a warning Warning: Pointer conversion to 'void (*)(void)' from 'const void *' discards qualifiers indicating that it does not expect a constant address here. Dropping the const and writing

static void fastcall (*myFunction)(void)=(void *)0xe544;

removes the warning and generates the same code. Also, static and __fastcall__ make no difference in the generated code and can be dropped.

Using a macro with inline assembler

This is where we leave pure C, but this is a method that works for generating short code:

 #define myFunction() (asm("jsr $e544"))

which translates into a single JSR command without the overhead of the function pointer approach.

Declaring the function as extern

Adding an extern function declaration

extern void myFunction(void);

makes the compiler also generate a single JSR command for the function call.

jsr myFunction

The extern could also have been omitted with the same result, since it is implicit for function declarations.

But there seems to be no way to give the C compiler the address of the external function in a C file. Defining the external function by assigning a function pointer as above gives an Redefinition of function 'myFunction' as different kind of symbol error.

Trying to pass the address as linker symbol with

cl65 --asm-define _myFunction=0xe544 example_function_by_address.c

gave Error: Symbol '_myFunction' is already defined error.

Filling the address with an external Assembler file

This completes the approach with the function declaration before:

The assembler names of C functions have an added _ at the beginning. So to provide an address for the C function myFunction, the assembler label _myFunction must be defined and exported. A possible assembler file would thus contain

.export _myFunction := $e544

Note that the Assembler file itself contains no code, it is only used to define the function address.

Linking both together

cl65 c-program-with-extern-and-function-call.c asm-export-prg.s -o test.prg

Generates a compact program as intended, however, it was not possible in pure C.

Summary

Pure C, but with overhead: void (*myFunction)(void)=(void *)0xe544;

With C file with inline assembler, bit ugly but no overhead: #define myFunction() (asm("jsr $e544"))

No overhead, but extra file: Function declaration and linking with an assembler file containing .export _myFunction := $e544

If anybody manages to get the version with the command line linker argument to run pls. let me know.

You must log in to answer this question.

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