Sviluppare in C per il Commodore 64 – parte 2

Sviluppare in C per il Commodore 64

In questa seconda parte affrontiamo una tematica che riguarda più nello specifico il linguaggio C ma che è fondamentale per poter affrontare progetti più complessi, e sopratutto per poter gestire il terzo ed ultimo articolo di questa serie che affronterà l’utilizzo della grafica.

File versus Progetti

Nel primo articolo abbiamo fatto qualcosa che rasenta il magico, ma ci siamo limitati a compilare un file. Se volessimo affrontare un progetto che abbia un minimo di complessità, non potremo di sicuro mettere tutto il codice in un solo file, anche solo per motivi di gestione, pulizia e leggibilità.

Ma come si fa a distribuire il codice in più moduli? E – sopratutto – come compilo più moduli, e quindi più file, in un unico eseguibile? Questo è quello che vedremo a breve.

Un progetto di saluti al mondo

Trasformeremo il classico esempio “hallo world” visto nell’articolo precedente in un progetto, e lo compileremo. Raggiungeno lo stesso risultato ma con una struttura simile a quella che potremo usare per ottenere risultati molto più complessi.

Prima di tutto, dobbiamo rendere un po più complesso il nostro ambiente. Creiamo una cartella di nome Hallo World ed al suo interno creiamo un’ulteriore cartella di nome src. Dentro quest’ultima inseriremo tutti i file sorgente del nostro progetto.

E senza perdere tempo creiamo il file halloworld.c dentro src con il seguente codice:

    #include <stdio.h>

    void halloworld(void) {
            printf("Hello, world!\n");
    }

Abbiamo trasferito la nostra “Business Logic” (Logica di Business, cioè la logica operativa di un programma) dentro un modulo (che appunto in C corrisponde ad un file) a se stante.

Ora ci serve sempre il codice principale, quello che avvia l’applicazione, per il quale creiamo il file main.c sempre dentro src. Tutti ci staremo immaginando una cosa del genere:

    void main(void) {
        halloworld();
    }

Siamo sicuri? Proviamo.

Make deus ex machina

Mi direte, si proviamo, ma come? I comandi necessari per compilare un progetto con più moduli sono molto più complessi e diversi, e cambiano col numero ed il tipo di moduli. Per questo di solito non si scrivono a mano ogni volta.

Fin dagli albori infatti in aiuto dei programmatori c’era il programma make, papà di tutti i software di “Build Automation”1 che esegue in file di comandi, di nome Makefile ed automatizza la compilazione. Noi faremo addirittura un passo oltre, ci creeremo un Makefile generico che vada bene per tutti i progetti che affronteremo, per lo meno fino a quando non avremo talmente dimestichezza e conoscenza del mezzo da avere la necessità ma anche la capacità di intervenire specificatamente nel processo di compilazione con parametri particolari.

I creatori di “CC65”, il sistema che stiamo usando, ci sono venuti incontro, e ne hanno creato uno per noi2. Personalmente ne ho creato una copia di un file che ho chiamato Makefile.orig e che uso come partenza per i progetti. Fatelo anche voi e mettetelo da qualche parte dove tenete i vostri progetti. Per comodità lo riporto di seguito:

    ###############################################################################
    ### Generic Makefile for cc65 projects - full version with abstract options ###
    ### V1.3.0(w) 2010 - 2013 Oliver Schmidt & Patryk "Silver Dream !" Łogiewa  ###
    ###############################################################################

    ###############################################################################
    ### In order to override defaults - values can be assigned to the variables ###
    ###############################################################################

    # Space or comma separated list of cc65 supported target platforms to build for.
    # Default: c64 (lowercase!)
    TARGETS :=

    # Name of the final, single-file executable.
    # Default: name of the current dir with target name appended
    PROGRAM :=

    # Path(s) to additional libraries required for linking the program
    # Use only if you don't want to place copies of the libraries in SRCDIR
    # Default: none
    LIBS    :=

    # Custom linker configuration file
    # Use only if you don't want to place it in SRCDIR
    # Default: none
    CONFIG  :=

    # Additional C compiler flags and options.
    # Default: none
    CFLAGS  =

    # Additional assembler flags and options.
    # Default: none
    ASFLAGS =

    # Additional linker flags and options.
    # Default: none
    LDFLAGS =

    # Path to the directory containing C and ASM sources.
    # Default: src
    SRCDIR :=

    # Path to the directory where object files are to be stored (inside respective target subdirectories).
    # Default: obj
    OBJDIR :=

    # Command used to run the emulator.
    # Default: depending on target platform. For default (c64) target: x64 -kernal kernal -VICIIdsize -autoload
    EMUCMD :=

    # Optional commands used before starting the emulation process, and after finishing it.
    # Default: none
    # Examples
    #PREEMUCMD := osascript -e "tell application \"System Events\" to set isRunning to (name of processes) contains \"X11.bin\"" -e "if isRunning is true then tell application \"X11\" to activate"
    #PREEMUCMD := osascript -e "tell application \"X11\" to activate"
    #POSTEMUCMD := osascript -e "tell application \"System Events\" to tell process \"X11\" to set visible to false"
    #POSTEMUCMD := osascript -e "tell application \"Terminal\" to activate"
    PREEMUCMD :=
    POSTEMUCMD :=

    # On Windows machines VICE emulators may not be available in the PATH by default.
    # In such case, please set the variable below to point to directory containing
    # VICE emulators. 
    #VICE_HOME := "C:\Program Files\WinVICE-2.2-x86\"
    VICE_HOME :=

    # Options state file name. You should not need to change this, but for those
    # rare cases when you feel you really need to name it differently - here you are
    STATEFILE := Makefile.options

    ###################################################################################
    ####  DO NOT EDIT BELOW THIS LINE, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!  ####
    ###################################################################################

    ###################################################################################
    ### Mapping abstract options to the actual compiler, assembler and linker flags ###
    ### Predefined compiler, assembler and linker flags, used with abstract options ###
    ### valid for 2.14.x. Consult the documentation of your cc65 version before use ###
    ###################################################################################

    # Compiler flags used to tell the compiler to optimise for SPEED
    define _optspeed_
      CFLAGS += -Oris
    endef

    # Compiler flags used to tell the compiler to optimise for SIZE
    define _optsize_
      CFLAGS += -Or
    endef

    # Compiler and assembler flags for generating listings
    define _listing_
      CFLAGS += --listing $$(@:.o=.lst)
      ASFLAGS += --listing $$(@:.o=.lst)
      REMOVES += $(addsuffix .lst,$(basename $(OBJECTS)))
    endef

    # Linker flags for generating map file
    define _mapfile_
      LDFLAGS += --mapfile $$@.map
      REMOVES += $(PROGRAM).map
    endef

    # Linker flags for generating VICE label file
    define _labelfile_
      LDFLAGS += -Ln $$@.lbl
      REMOVES += $(PROGRAM).lbl
    endef

    # Linker flags for generating a debug file
    define _debugfile_
      LDFLAGS += -Wl --dbgfile,$$@.dbg
      REMOVES += $(PROGRAM).dbg
    endef

    ###############################################################################
    ###  Defaults to be used if nothing defined in the editable sections above  ###
    ###############################################################################

    # Presume the C64 target like the cl65 compile & link utility does.
    # Set TARGETS to override.
    ifeq ($(TARGETS),)
      TARGETS := c64
    endif

    # Presume we're in a project directory so name the program like the current
    # directory. Set PROGRAM to override.
    ifeq ($(PROGRAM),)
      PROGRAM := $(notdir $(CURDIR))
    endif

    # Presume the C and asm source files to be located in the subdirectory 'src'.
    # Set SRCDIR to override.
    ifeq ($(SRCDIR),)
      SRCDIR := src
    endif

    # Presume the object and dependency files to be located in the subdirectory
    # 'obj' (which will be created). Set OBJDIR to override.
    ifeq ($(OBJDIR),)
      OBJDIR := obj
    endif
    TARGETOBJDIR := $(OBJDIR)/$(TARGETS)

    # Default emulator commands and options for particular targets.
    # Set EMUCMD to override.
    c64_EMUCMD := $(VICE_HOME)x64 -kernal kernal -VICIIdsize -autoload
    c128_EMUCMD := $(VICE_HOME)x128 -kernal kernal -VICIIdsize -autoload
    vic20_EMUCMD := $(VICE_HOME)xvic -kernal kernal -VICdsize -autoload
    pet_EMUCMD := $(VICE_HOME)xpet -Crtcdsize -autoload
    plus4_EMUCMD := $(VICE_HOME)xplus4 -TEDdsize -autoload
    # So far there is no x16 emulator in VICE (why??) so we have to use xplus4 with -memsize option
    c16_EMUCMD := $(VICE_HOME)xplus4 -ramsize 16 -TEDdsize -autoload
    cbm510_EMUCMD := $(VICE_HOME)xcbm2 -model 510 -VICIIdsize -autoload
    cbm610_EMUCMD := $(VICE_HOME)xcbm2 -model 610 -Crtcdsize -autoload
    atari_EMUCMD := atari800 -windowed -xl -pal -nopatchall -run

    ifeq ($(EMUCMD),)
      EMUCMD = $($(CC65TARGET)_EMUCMD)
    endif

    ###############################################################################
    ### The magic begins                                                        ###
    ###############################################################################

    # The "Native Win32" GNU Make contains quite some workarounds to get along with
    # cmd.exe as shell. However it does not provide means to determine that it does
    # actually activate those workarounds. Especially $(SHELL) does NOT contain the
    # value 'cmd.exe'. So the usual way to determine if cmd.exe is being used is to
    # execute the command 'echo' without any parameters. Only cmd.exe will return a
    # non-empty string - saying 'ECHO is on/off'.
    #
    # Many "Native Win32" programs accept '/' as directory delimiter just fine. How-
    # ever the internal commands of cmd.exe generally require '\' to be used.
    #
    # cmd.exe has an internal command 'mkdir' that doesn't understand nor require a
    # '-p' to create parent directories as needed.
    #
    # cmd.exe has an internal command 'del' that reports a syntax error if executed
    # without any file so make sure to call it only if there's an actual argument.
    ifeq ($(shell echo),)
      MKDIR = mkdir -p $1
      RMDIR = rmdir $1
      RMFILES = $(RM) $1
    else
      MKDIR = mkdir $(subst /,\,$1)
      RMDIR = rmdir $(subst /,\,$1)
      RMFILES = $(if $1,del /f $(subst /,\,$1))
    endif
    COMMA := ,
    SPACE := $(N/A) $(N/A)
    define NEWLINE


    endef
    # Note: Do not remove any of the two empty lines above !

    TARGETLIST := $(subst $(COMMA),$(SPACE),$(TARGETS))

    ifeq ($(words $(TARGETLIST)),1)

    # Set PROGRAM to something like 'myprog.c64'.
    override PROGRAM := $(PROGRAM).$(TARGETLIST)

    # Set SOURCES to something like 'src/foo.c src/bar.s'.
    # Use of assembler files with names ending differently than .s is deprecated!
    SOURCES := $(wildcard $(SRCDIR)/*.c)
    SOURCES += $(wildcard $(SRCDIR)/*.s)
    SOURCES += $(wildcard $(SRCDIR)/*.asm)
    SOURCES += $(wildcard $(SRCDIR)/*.a65)

    # Add to SOURCES something like 'src/c64/me.c src/c64/too.s'.
    # Use of assembler files with names ending differently than .s is deprecated!
    SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.c)
    SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.s)
    SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.asm)
    SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.a65)

    # Set OBJECTS to something like 'obj/c64/foo.o obj/c64/bar.o'.
    OBJECTS := $(addsuffix .o,$(basename $(addprefix $(TARGETOBJDIR)/,$(notdir $(SOURCES)))))

    # Set DEPENDS to something like 'obj/c64/foo.d obj/c64/bar.d'.
    DEPENDS := $(OBJECTS:.o=.d)

    # Add to LIBS something like 'src/foo.lib src/c64/bar.lib'.
    LIBS += $(wildcard $(SRCDIR)/*.lib)
    LIBS += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.lib)

    # Add to CONFIG something like 'src/c64/bar.cfg src/foo.cfg'.
    CONFIG += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.cfg)
    CONFIG += $(wildcard $(SRCDIR)/*.cfg)

    # Select CONFIG file to use. Target specific configs have higher priority.
    ifneq ($(word 2,$(CONFIG)),)
      CONFIG := $(firstword $(CONFIG))
      $(info Using config file $(CONFIG) for linking)
    endif

    .SUFFIXES:
    .PHONY: all test clean zap love

    all: $(PROGRAM)

    -include $(DEPENDS)
    -include $(STATEFILE)

    # If OPTIONS are given on the command line then save them to STATEFILE
    # if (and only if) they have actually changed. But if OPTIONS are not
    # given on the command line then load them from STATEFILE. Have object
    # files depend on STATEFILE only if it actually exists.
    ifeq ($(origin OPTIONS),command line)
      ifneq ($(OPTIONS),$(_OPTIONS_))
        ifeq ($(OPTIONS),)
          $(info Removing OPTIONS)
          $(shell $(RM) $(STATEFILE))
          $(eval $(STATEFILE):)
        else
          $(info Saving OPTIONS=$(OPTIONS))
          $(shell echo _OPTIONS_=$(OPTIONS) > $(STATEFILE))
        endif
        $(eval $(OBJECTS): $(STATEFILE))
      endif
    else
      ifeq ($(origin _OPTIONS_),file)
        $(info Using saved OPTIONS=$(_OPTIONS_))
        OPTIONS = $(_OPTIONS_)
        $(eval $(OBJECTS): $(STATEFILE))
      endif
    endif

    # Transform the abstract OPTIONS to the actual cc65 options.
    $(foreach o,$(subst $(COMMA),$(SPACE),$(OPTIONS)),$(eval $(_$o_)))

    # Strip potential variant suffix from the actual cc65 target.
    CC65TARGET := $(firstword $(subst .,$(SPACE),$(TARGETLIST)))

    # The remaining targets.
    $(TARGETOBJDIR):
        $(call MKDIR,$@)

    vpath %.c $(SRCDIR)/$(TARGETLIST) $(SRCDIR)

    $(TARGETOBJDIR)/%.o: %.c | $(TARGETOBJDIR)
        cl65 -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(CFLAGS) -o $@ $<

    vpath %.s $(SRCDIR)/$(TARGETLIST) $(SRCDIR)

    $(TARGETOBJDIR)/%.o: %.s | $(TARGETOBJDIR)
        cl65 -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<

    vpath %.asm $(SRCDIR)/$(TARGETLIST) $(SRCDIR)

    $(TARGETOBJDIR)/%.o: %.asm | $(TARGETOBJDIR)
        cl65 -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<

    vpath %.a65 $(SRCDIR)/$(TARGETLIST) $(SRCDIR)

    $(TARGETOBJDIR)/%.o: %.a65 | $(TARGETOBJDIR)
        cl65 -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<

    $(PROGRAM): $(CONFIG) $(OBJECTS) $(LIBS)
        cl65 -t $(CC65TARGET) $(LDFLAGS) -o $@ $(patsubst %.cfg,-C %.cfg,$^)

    test: $(PROGRAM)
        $(PREEMUCMD)
        $(EMUCMD) $<
        $(POSTEMUCMD)

    clean:
        $(call RMFILES,$(OBJECTS))
        $(call RMFILES,$(DEPENDS))
        $(call RMFILES,$(REMOVES))
        $(call RMFILES,$(PROGRAM))

    else # $(words $(TARGETLIST)),1

    all test clean:
        $(foreach t,$(TARGETLIST),$(MAKE) TARGETS=$t $@$(NEWLINE))

    endif # $(words $(TARGETLIST)),1

    OBJDIRLIST := $(wildcard $(OBJDIR)/*)

    zap:
        $(foreach o,$(OBJDIRLIST),-$(call RMFILES,$o/*.o $o/*.d $o/*.lst)$(NEWLINE))
        $(foreach o,$(OBJDIRLIST),-$(call RMDIR,$o)$(NEWLINE))
        -$(call RMDIR,$(OBJDIR))
        -$(call RMFILES,$(basename $(PROGRAM)).* $(STATEFILE))

    love:
        @echo "Not war, eh?"

    ###################################################################
    ###  Place your additional targets in the additional Makefiles  ###
    ### in the same directory - their names have to end with ".mk"! ###
    ###################################################################
    -include *.mk

Poi prepariamolo per il nostro progetto. Facciamo una copia di Makefile.orig chiamandolo Makefile dentro la cartella Hallo World e non dentro src.

Prepariamo il Makefile

Ora modifichiamolo per i nostri scopi, e approfittiamone per vederne le parti principali.

Alla riga 12 troviamo

    TARGETS :=

Nell’articolo precedente abbiamo visto cosa sono i target, e questo è il posto dove indicarli usando delle stringhe definite dal progetto “CC65”3 e separate da virgola. Ad esempio aggiungendo apple2,c64 verrà creato un programma per Apple2 ed uno per Commodore 64. Se non inseriamo nulla, invece, come faremo noi ora, si intenderà di default il solo Commodore 64.

Alla riga 16 troviamo

    PROGRAM :=

Questo parametro serve per dare il nome all’eseguibile finale, e noi scriviamoci hw.

Seguono parametri che permettono di indicare librerie particolari da usare, opzioni particolari per il compilatore, l’assembler ed il linker, ma che per ora non useremo.

E poi alla riga 42 troviamo

    SRCDIR :=

Se non la modifichiamo, di default cercherà i file sorgenti dentro la cartella src, come abbiamo fatto nell’esempio. Se vogliamo cambiare il nome di questa cartella, dovremo indicarlo quì.

Per ora questi sono tutti i parametri che ci interessano. Da linea di comando, all’interno della cartella Hallo World diamo il comando

    make

Verrà interpretato ed eseguito il Makefile ed avremo… un errore?

Vedrete una cosa simile a

    cl65 -t c64 -c --create-dep obj/c64/halloworld.d  -o obj/c64/halloworld.o src/halloworld.c
    cl65 -t c64 -c --create-dep obj/c64/main.d  -o obj/c64/main.o src/main.c src/main.c(2): Error: Call to undefined function `halloworld'
    make: *** [obj/c64/main.o] Error 1

Funzione mia non ti conosco

Il contenuto di main.c non è solo quello che abbiamo immaginato prima, perchè quando al suo interno invochiamo la funzione halloworld quel specifico modulo non sa cosa sia, ed infatti otteniamo un errore di “chiamata a funzione non definita”. Perchè la definizione c’è, ma è dentro il modulo “halloworld”, dobbiamo fare in modo che ne venga a conoscenza anche il modulo “main”: e ci vengo in aiuto i file headers, quello che terminano con .h.

Proviamo; creiamo dentro src un file che chiameremo halloworld.h il cui contenuto sarà:

    #ifndef HALLOWORLD
    #define HALLOWORLD

    void halloworld(void);

    #endif

Le direttive che sono precedute da # sono eseguite dal preprocessore – uno dei comandi della catena di compilazione – ed in questo caso servono a fare in modo che se anche questo file header venga incluso più di una volta, il suo contenuto sarà processato una volta sola. La parte centrale definisce la nostra funzione halloworld per fare in modo che un modulo ne sia a conoscenza. Ma come fa il nostro moulo “main” ad usare questa definizione? Vediamo il file main.c corretto:

    #include "halloworld.h"

    void main(void) {
        halloworld();
    }

Vedrete che ora compilando col comando make non avremo errori.

Salutiamo il mondo

Nella nostra cartella avremo ora in file hw.c64, chiamato così perchè prima abbiamo indicato di voler chiamare hw i nostri eseguibile, e .c64 per indicare il target per il quale è valido il programma. Facciamone una copia chiamandolo hw.prg ed eseguiamolo, et voilà!


  1. https://en.wikipedia.org/wiki/Make_(software)#Makefile ↩︎
  2. https://github.com/cc65/wiki/wiki/Bigger-Projects ↩︎
  3. Per sapere quali sono tutti i target disponibili la via più veloce è da linea di comando eseguire cl65 --list-targets ↩︎