I know it's almost heresy to ask something like this, but ...

We have C compilers for the Z80/6502 and there used to exist a thing called the Comeau C++ compiler, that produced C code as output. There also used to exist the possibility to compile llvm code into C and I know there was an effort to write a z80 backend for llvm.

I am not really interested in the usual MIPS → Z80/6502 and x86 → Z80/6502 translation approaches. Does there exist a way to conjure a C++ toolchain for the Z80 or (gasp) the 6502/6510?

I'd like to see what std::cout << "Hello word!" << std::endl; would compile into and, of course, run this program on a speccy or a C64.

  • 6
    Anyway, your compiled program would run out of memory after the "r" ;)
    – Brian H
    Jul 1 at 21:06
  • I had thought I used Turbo C++ on CP/M, but looking it up it didn't come out until 1990 - far past when anybody cared about CP/M or Z80s. Must have been thinking of Turbo Pascal. Jul 1 at 22:37
  • @BrianH I'am not so sure, people generally don't have time for something like this, that's all. Jul 2 at 9:28
  • @user1095108 It's a fine question...I was just punning on your typo.
    – Brian H
    Jul 2 at 14:37

Using this code as a starting point:

extern "C" {
    void xputc(int);
    void xputs(const char *);

class ostream {
    ostream &operator<<(const char *);
    ostream &operator<<(char c);


ostream &ostream::operator<<(const char *cp) {
    return *this;

ostream &ostream::operator<<(char c) {
    return *this;

static ostream cout;
static const char endl = '\n';

int main(int argc, char **argv) {

    cout << "hello, world" << endl;
    return 0;

(Note that there's no 6502/z80 c++ lib so I wrote my own minimal cout implementation. It might be possible to include cc65's <stdio.h> directly but I chose to handle that below in the interest of expediency.)

I ran it through MPW CFront (3.2, based on AT&T 2.1.12) which generated this monstrosity:

#line 1 "hello.cpp"

/* << CFront Version 3.2 (1/27/92; AT&T 2.1.12) �Apple Computer, Inc.1989-92 >> */
/* < hello.cpp > */
extern void *__vec_new__FPviT2T1(void *, int , int , void *);
extern void __vec_delete__FPviT2T1N22(void *, int , int , void *, int , int );typedef void (*__vptp)();
struct __mptr {short d;short i;__vptp f;};
static struct __mptr __zero_mptr ={0,0,0};/*dead-stripped if not used*/

#line 1 "hello.cpp"
extern struct __mptr* __ptbl_vec__hello_cpp_[ ] ;
void **__nw__12PascalObjectSFPFv_vUi(pascal void (*)(void ), unsigned int );
void __dl__12PascalObjectSFPPv(void **);
struct PascalObject;
void **__nw__12HandleObjectSFUi(unsigned int );
void __dl__12HandleObjectSFPPv(void **);
struct HandleObject;
struct SingleObject;
struct ostream;

#line 7 "hello.cpp"
struct ostream {    /* sizeof ostream == 2 */

#line 12 "hello.cpp"
char __W1;
extern void xputs(char *);
extern struct __mptr* __ptbl_vec__hello_cpp_[ ] ;

#line 14 "hello.cpp"
struct ostream *__ls__7ostreamFPCc(struct ostream *this, char *cp){ 
#line 15 "hello.cpp"
xputs( cp) ;

#line 16 "hello.cpp"
return (struct ostream *)this;
extern void xputc(int );

#line 19 "hello.cpp"
struct ostream *__ls__7ostreamFc(struct ostream *this, char c){ 
#line 20 "hello.cpp"
xputc( (int )c) ;

#line 21 "hello.cpp"
return (struct ostream *)this;

#line 25 "hello.cpp"
static struct ostream cout;

#line 26 "hello.cpp"

#line 28 "hello.cpp"
int main(int argc, char **argv){{ 
#line 30 "hello.cpp"
__ls__7ostreamFc( (struct ostream *)__ls__7ostreamFPCc( & cout, (char *)"hello, world")
#line 31 "hello.cpp"
, (char )((char )13)) ;
return (int )0;

#line 32 "hello.cpp"
/* the end */

Careful observers will note that MPW's CFront converts the endl \n to be \r which was the classic Macintosh standard end of line character.

I manually commented out the void **__nw__12PascalObjectSFPFv_vUi(pascal void (*)(void ), unsigned int ); line (which cc65 did not like) and added these lines to get working i/o:

#include <stdio.h>

#define xputc(x) fputc(x, stdout)
#define xputs(x) fputs(x, stdout)

Running that code through cc65, generates this assembly, which is actually not that bad:

; File generated by cc65 v 2.18 - N/A
    .fopt       compiler,"cc65 v 2.18 - N/A"
    .setcpu     "6502"
    .smart      on
    .autoimport on
    .case       on
    .debuginfo  off
    .importzp   sp, sreg, regsave, regbank
    .importzp   tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4
    .macpack    longbranch
    .forceimport    __STARTUP__
    .forceimport    initmainargs
    .import     _stdout
    .import     _fputc
    .import     _fputs
    .export     ___ls__7ostreamFPCc
    .export     ___ls__7ostreamFc
    .export     _main

.segment    "DATA"

    .word   $0000
    .word   $0000
    .word   $0000

.segment    "RODATA"

    .byte   $68,$65,$6C,$6C,$6F,$2C,$20,$77,$6F,$72,$6C,$64,$00

.segment    "BSS"

    .res    1,$00

; ---------------------------------------------------------------
; __near__ struct ostream * __near__ __ls__7ostreamFPCc (__near__ struct ostream *, __near__ unsigned char *)
; ---------------------------------------------------------------

.segment    "CODE"

.proc   ___ls__7ostreamFPCc: near

.segment    "CODE"

    jsr     pushax
    ldy     #$01
    jsr     ldaxysp
    jsr     pushax
    lda     _stdout
    ldx     _stdout+1
    jsr     _fputs
    ldy     #$03
    jsr     ldaxysp
    jmp     L0004
L0004:  jsr     incsp4


; ---------------------------------------------------------------
; __near__ struct ostream * __near__ __ls__7ostreamFc (__near__ struct ostream *, unsigned char)
; ---------------------------------------------------------------

.segment    "CODE"

.proc   ___ls__7ostreamFc: near

.segment    "CODE"

    jsr     pusha
    ldy     #$00
    ldx     #$00
    lda     (sp),y
    ldx     #$00
    jsr     pushax
    lda     _stdout
    ldx     _stdout+1
    jsr     _fputc
    ldy     #$02
    jsr     ldaxysp
    jmp     L0009
L0009:  jsr     incsp3


; ---------------------------------------------------------------
; int __near__ __cdecl__ main (int, __near__ __near__ unsigned char * *)
; ---------------------------------------------------------------

.segment    "CODE"

.proc   _main: near

.segment    "CODE"

    lda     #<(_cout)
    ldx     #>(_cout)
    jsr     pushax
    lda     #<(L0013)
    ldx     #>(L0013)
    jsr     ___ls__7ostreamFPCc
    jsr     pushax
    lda     #$0D
    jsr     ___ls__7ostreamFc
    ldx     #$00
    lda     #$00
    jmp     L000E
L000E:  jsr     incsp4


The C code was also compiled and run with clang (on x64) and ORCA/C (on 65816) to verify it was valid.

  • 1
    Using CFront is truly retro, hopefully others will take up the challenge. Jul 1 at 13:33

llvm-mos compiles a considerable subset of C++ to 6502 machine code out of the box. IIRC it's just missing "runtime support"; things like RTTI, exceptions, and the runtime layout of VTables. The last one is kinda important, but I don't think it's too likely that there are any compiler changes necessary to get that working; just "standard library" stuff. Should be roughly the equivalent of writing crt0.s for C.

Oh, and static constructors/destructors running before/after main. That would also take some _start work.

Still, quite a lot does work out of the box. We've even implemented our C multiplication libcalls in C++ for our standard library. C++ templates really reduce code duplication, now even for the 6502!



The most efficient way to process a C++ dialect on the 6502 or Z80 would probably be to design a virtual machine analogous to the UCSD P-system and then write a compiler that targets that. Although processing bytecode would add some overhead, using a chrget routine somewhat like the one used by BASIC, but including a dispatcher:

  ldx nextByte
  inc loadPatch+1
  bne doIt
  inc loadPatch+2
  lda jumps,x
  sta execPatch+1
  jmp execTable

could reduce the overhead of instruction decoding to be in line with the overhead of trying to manage automatic-duration objects, especially since one could have families of instructions that exploit the values in the X register. For example, one could emulate a machine with a 16-bit accumulator and four general-purpose 16-bit registers, and have an instruction handlers like:

addRegToAcc: ; Even-numbered opcodes $40 to $46
  lda   regs-$40,x
  adc   acc16l
  sta   acc16l
  lda   regs-$40+1,x
  adc   acc16h
  sta   acc16h
  jmp   mainExec
addAccToReg: ; Odd-numbered opcodes $41 to $47
  lda   regs-$41,x
  adc   acc16l
  sta   regs-$41,x
  lda   regs-$41+1,x
  adc   acc16h
  sta   regs-$41+1,x
  jmp   mainExec

The time to dispatch and execute such an instruction would likely be 22 cycles for dispatching and 25 or 27 for the actual execution. While that dispatching overhead would be significant, its cost should be balanced against the fact that the VM is able to use a single byte instruction to add a 16-bit accumulator to one of four 16-bit virtual registers, as opposed to having to use a minimum of 12 bytes of native machine code (which would take a minimum of 18 cycles to execute) to accomplish that task.

  • So it would be about 2.6 times slower? Aug 18 at 23:36
  • @BruceAbbott: Yes, compared to the fastest possible, but rather bulky, machine code. The 6502 supports a rather wide range of possible speed/space trade-offs, and if 10% of the time would be responsible for 90% of the execution time, cutting the size of 90% of the code by half could in many cases free up enough space to make the remaining 10% of the code more than 25% faster, offering a net performance win.
    – supercat
    Aug 19 at 14:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.